@mistertemp/libs-front-shared 3.0.4 → 4.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/package.json +10 -1
  2. package/src/components/Notifications/CustomerNotificationCenterClient.ts +195 -0
  3. package/src/components/Notifications/EmptyNotificationsList.tsx +33 -0
  4. package/src/components/Notifications/Notification.tsx +105 -0
  5. package/src/components/Notifications/NotificationContent.tsx +153 -0
  6. package/src/components/Notifications/Notifications.module.scss +345 -0
  7. package/src/components/Notifications/NotificationsBell.tsx +242 -0
  8. package/src/components/Notifications/NotificationsCenterContext.tsx +578 -0
  9. package/src/components/Notifications/NotificationsList.tsx +293 -0
  10. package/src/components/Notifications/PopupNotifications.module.scss +73 -0
  11. package/src/components/Notifications/PopupNotifications.tsx +42 -0
  12. package/src/components/Notifications/dateISOToString.ts +32 -0
  13. package/src/components/Notifications/index.ts +7 -0
  14. package/src/components/Notifications/types.ts +39 -0
  15. package/src/components/Notifications/useLiveNotifications.ts +122 -0
  16. package/src/components/index.ts +1 -0
  17. package/src/locales/es/date-picker.json +11 -0
  18. package/src/locales/es/notification-error.json +12 -0
  19. package/src/locales/es/notification-utils.json +7 -0
  20. package/src/locales/es/notification.json +17 -0
  21. package/src/locales/fr/notification-error.json +12 -0
  22. package/src/locales/fr/notification-utils.json +7 -0
  23. package/src/locales/fr/notification.json +17 -0
  24. package/src/locales/it/notification-error.json +12 -0
  25. package/src/locales/it/notification-utils.json +7 -0
  26. package/src/locales/it/notification.json +17 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mistertemp/libs-front-shared",
3
- "version": "3.0.4",
3
+ "version": "4.0.0-alpha.0",
4
4
  "main": "index.ts",
5
5
  "license": "UNLICENSED",
6
6
  "scripts": {
@@ -20,12 +20,17 @@
20
20
  "stylelint": {
21
21
  "extends": "./.stylelintrc"
22
22
  },
23
+ "dependencies": {
24
+ "classnames": "^2.5.1"
25
+ },
23
26
  "devDependencies": {
24
27
  "@eslint/compat": "2.0.5",
25
28
  "@eslint/eslintrc": "3.3.5",
26
29
  "@eslint/js": "9.39.4",
27
30
  "@mistertemp/design-system": "14.0.0",
31
+ "@mistertemp/front-core": "19.3.0",
28
32
  "@mistertemp/libs-work-environments": "0.8.1",
33
+ "@mistertemp/types-agency-notification-center": "4.3.0",
29
34
  "@mistertemp/types-shared": "2.7.0",
30
35
  "@testing-library/dom": "10.4.1",
31
36
  "@testing-library/jest-dom": "6.9.0",
@@ -78,7 +83,11 @@
78
83
  },
79
84
  "peerDependencies": {
80
85
  "@mistertemp/design-system": ">=14.0.0",
86
+ "@mistertemp/front-core": ">=19.3.0",
81
87
  "@mistertemp/libs-work-environments": "^0.8.1",
88
+ "@mistertemp/types-agency-notification-center": ">=4.3.0",
89
+ "classnames": "^2.5.1",
90
+ "date-fns": ">=3.6.0",
82
91
  "i18next": "^25.4.2",
83
92
  "i18next-browser-languagedetector": ">=8.2.0",
84
93
  "i18next-http-backend": ">=3.0.2",
@@ -0,0 +1,195 @@
1
+ import { NotificationTypes, PublishedNotification } from '@mistertemp/types-agency-notification-center';
2
+
3
+ import { NotificationCenterClientInterface } from './types';
4
+
5
+ const BASE_URLS: Record<string, string> = {
6
+ dev: 'https://customer-notification-center-back.development.mistertemp.io',
7
+ sta: 'https://customer-notification-center-back.staging.mistertemp.io',
8
+ prod: 'https://customer-notification-center-back.mistertemp.io',
9
+ };
10
+
11
+ /**
12
+ * Customer notification center client.
13
+ * Adapts the customer back-end API to the common NotificationCenterClientInterface.
14
+ *
15
+ * The interface uses `agencyId` as param name for compatibility with the shared context,
16
+ * but it maps to `accountId` when calling the customer API.
17
+ */
18
+ export class CustomerNotificationCenterClient implements NotificationCenterClientInterface {
19
+ private baseUrl: string;
20
+
21
+ constructor(envOrBaseUrl: 'dev' | 'sta' | 'prod' | string = 'dev') {
22
+ if (envOrBaseUrl in BASE_URLS) {
23
+ this.baseUrl = BASE_URLS[envOrBaseUrl as keyof typeof BASE_URLS];
24
+ } else {
25
+ // Allow passing a direct base URL
26
+ this.baseUrl = envOrBaseUrl;
27
+ }
28
+ }
29
+
30
+ async getNotificationList(params: {
31
+ userCorrelationId: string;
32
+ agencyId: string; // maps to accountId
33
+ token: string;
34
+ queryString?: Record<string, unknown>;
35
+ }): Promise<PublishedNotification<NotificationTypes>[]> {
36
+ const { userCorrelationId, agencyId: accountId, token, queryString } = params;
37
+
38
+ const url = new URL(
39
+ `/customer/v1/users/${userCorrelationId}/notifications`,
40
+ this.baseUrl,
41
+ );
42
+ url.searchParams.set('accountId', accountId);
43
+
44
+ if (queryString?.isRead !== undefined && queryString.isRead !== null) {
45
+ url.searchParams.set('isRead', String(queryString.isRead));
46
+ }
47
+ if (queryString?.categories) {
48
+ url.searchParams.set('categories', String(queryString.categories));
49
+ }
50
+ if (queryString?.startDate) {
51
+ url.searchParams.set('startDate', String(queryString.startDate));
52
+ }
53
+ if (queryString?.endDate) {
54
+ url.searchParams.set('endDate', String(queryString.endDate));
55
+ }
56
+
57
+ const allItems: PublishedNotification<NotificationTypes>[] = [];
58
+ let nextCursor: string | undefined;
59
+
60
+ // Fetch all pages (cursor-based pagination)
61
+ do {
62
+ if (nextCursor) {
63
+ url.searchParams.set('cursor', nextCursor);
64
+ }
65
+
66
+ const response = await fetch(url.toString(), {
67
+ method: 'GET',
68
+ headers: {
69
+ Authorization: `Bearer ${token}`,
70
+ 'Content-Type': 'application/json',
71
+ },
72
+ });
73
+
74
+ if (!response.ok) {
75
+ throw new Error(`Failed to fetch notifications: ${response.status}`);
76
+ }
77
+
78
+ const data = await response.json();
79
+ allItems.push(...(data.items || []));
80
+ nextCursor = data.nextCursor;
81
+ } while (nextCursor);
82
+
83
+ return allItems;
84
+ }
85
+
86
+ async getNotificationStatus(params: {
87
+ userCorrelationId: string;
88
+ agencyId: string; // maps to accountId
89
+ token: string;
90
+ }): Promise<{ countUnread: number; countUnseen: number }> {
91
+ const { userCorrelationId, agencyId: accountId, token } = params;
92
+
93
+ const url = new URL(
94
+ `/customer/v1/users/${userCorrelationId}/notifications/status`,
95
+ this.baseUrl,
96
+ );
97
+ url.searchParams.set('accountId', accountId);
98
+
99
+ const response = await fetch(url.toString(), {
100
+ method: 'GET',
101
+ headers: {
102
+ Authorization: `Bearer ${token}`,
103
+ 'Content-Type': 'application/json',
104
+ },
105
+ });
106
+
107
+ if (!response.ok) {
108
+ throw new Error(`Failed to fetch notification status: ${response.status}`);
109
+ }
110
+
111
+ return response.json();
112
+ }
113
+
114
+ async markNotificationAsRead(params: {
115
+ userCorrelationId: string;
116
+ agencyId: string;
117
+ notificationId: string;
118
+ token: string;
119
+ }): Promise<unknown> {
120
+ const { userCorrelationId, notificationId, token } = params;
121
+
122
+ const response = await fetch(
123
+ `${this.baseUrl}/customer/v1/users/${userCorrelationId}/notifications/${encodeURIComponent(notificationId)}/mark-as-read`,
124
+ {
125
+ method: 'PATCH',
126
+ headers: {
127
+ Authorization: `Bearer ${token}`,
128
+ 'Content-Type': 'application/json',
129
+ },
130
+ },
131
+ );
132
+
133
+ if (!response.ok) {
134
+ throw new Error(`Failed to mark notification as read: ${response.status}`);
135
+ }
136
+
137
+ return {};
138
+ }
139
+
140
+ async markAllNotificationAsRead(params: {
141
+ userCorrelationId: string;
142
+ agencyId: string; // maps to accountId
143
+ token: string;
144
+ }): Promise<unknown> {
145
+ const { userCorrelationId, agencyId: accountId, token } = params;
146
+
147
+ const url = new URL(
148
+ `/customer/v1/users/${userCorrelationId}/notifications/mark-all-as-read`,
149
+ this.baseUrl,
150
+ );
151
+ url.searchParams.set('accountId', accountId);
152
+
153
+ const response = await fetch(url.toString(), {
154
+ method: 'PATCH',
155
+ headers: {
156
+ Authorization: `Bearer ${token}`,
157
+ 'Content-Type': 'application/json',
158
+ },
159
+ });
160
+
161
+ if (!response.ok) {
162
+ throw new Error(`Failed to mark all notifications as read: ${response.status}`);
163
+ }
164
+
165
+ return {};
166
+ }
167
+
168
+ async markAlertAsOpen(params: {
169
+ userCorrelationId: string;
170
+ agencyId: string; // maps to accountId
171
+ token: string;
172
+ }): Promise<unknown> {
173
+ const { userCorrelationId, agencyId: accountId, token } = params;
174
+
175
+ const url = new URL(
176
+ `/customer/v1/users/${userCorrelationId}/notifications/mark-alert-as-open`,
177
+ this.baseUrl,
178
+ );
179
+ url.searchParams.set('accountId', accountId);
180
+
181
+ const response = await fetch(url.toString(), {
182
+ method: 'PATCH',
183
+ headers: {
184
+ Authorization: `Bearer ${token}`,
185
+ 'Content-Type': 'application/json',
186
+ },
187
+ });
188
+
189
+ if (!response.ok) {
190
+ throw new Error(`Failed to mark alert as open: ${response.status}`);
191
+ }
192
+
193
+ return {};
194
+ }
195
+ }
@@ -0,0 +1,33 @@
1
+ import { FeaturedIcon, MagnifyingGlassSvg } from '@mistertemp/design-system';
2
+ import React, { FC } from 'react';
3
+
4
+ import { useBundledTranslation } from '../../hooks';
5
+ import fr from '../../locales/fr/notification.json';
6
+ import it from '../../locales/it/notification.json';
7
+ import es from '../../locales/es/notification.json';
8
+ import styles from './Notifications.module.scss';
9
+
10
+ const translationNs = 'notification';
11
+ const translations = { it, fr, es };
12
+
13
+ export const EmptyNotificationsList: FC = () => {
14
+ const { t } = useBundledTranslation(translationNs, translations);
15
+
16
+ return (
17
+ <div data-testid="emptyNotificationsList" className={styles.emptyNotificationsList}>
18
+ <FeaturedIcon
19
+ className={styles.emptyNotificationsListIcon}
20
+ icon={MagnifyingGlassSvg}
21
+ color="gray"
22
+ />
23
+ <span className={styles.emptyNotificationsListTitle}>
24
+ {t('notification:notificationsPopover.noNotifications')}
25
+ </span>
26
+ <span className={styles.emptyNotificationsListContent}>
27
+ {t('notification:notificationsPopover.lastActivityWillDisplayHere')}
28
+ </span>
29
+ </div>
30
+ );
31
+ };
32
+
33
+ export default EmptyNotificationsList;
@@ -0,0 +1,105 @@
1
+ import { DotSvg } from '@mistertemp/design-system';
2
+ import {
3
+ AggregatedNotificationMessage,
4
+ NotificationTypes,
5
+ } from '@mistertemp/types-agency-notification-center';
6
+ import classNames from 'classnames';
7
+ import React, { FC, useContext, useMemo, useState } from 'react';
8
+
9
+ import { useBundledTranslation } from '../../hooks';
10
+ import es from '../../locales/es/notification-utils.json';
11
+ import fr from '../../locales/fr/notification-utils.json';
12
+ import it from '../../locales/it/notification-utils.json';
13
+ import { dateISOToString } from './dateISOToString';
14
+ import { NotificationsCenterContext } from './NotificationsCenterContext';
15
+ import NotificationContent from './NotificationContent';
16
+ import styles from './Notifications.module.scss';
17
+
18
+ const translationNs = 'notification-utils';
19
+ const translations = { it, fr, es };
20
+
21
+ interface NotificationProps {
22
+ 'data-testid'?: string;
23
+ id: string;
24
+ title: string;
25
+ createdAt: string;
26
+ content: string;
27
+ onClose?: () => void;
28
+ link: string;
29
+ isUnread: boolean;
30
+ notificationType: NotificationTypes;
31
+ rank: number;
32
+ aggregatedSubtitle?: string;
33
+ aggregatedNotifications?: AggregatedNotificationMessage[];
34
+ }
35
+
36
+ export const Notification: FC<NotificationProps> = ({
37
+ 'data-testid': dataTestId,
38
+ id,
39
+ title,
40
+ createdAt,
41
+ content,
42
+ onClose,
43
+ link,
44
+ isUnread,
45
+ notificationType,
46
+ rank,
47
+ aggregatedSubtitle,
48
+ aggregatedNotifications,
49
+ }) => {
50
+ const { t } = useBundledTranslation(translationNs, translations);
51
+ const { isFullPageMode, markNotificationAsRead } = useContext(NotificationsCenterContext);
52
+ const formattedDate = useMemo(() => dateISOToString(createdAt), [createdAt]);
53
+ const [expandedContent, setExpandedContent] = useState(false);
54
+
55
+ const date = useMemo(
56
+ () => t(`notification-utils:dateISOToString.${formattedDate.i18n}`, { value: formattedDate.value }),
57
+ [t, formattedDate],
58
+ );
59
+
60
+ const toggleExpandContent = () => {
61
+ setExpandedContent((prev) => !prev);
62
+ };
63
+
64
+ const handleNotificationClick = () => {
65
+ markNotificationAsRead(id);
66
+ };
67
+
68
+ return (
69
+ <div
70
+ data-testid={`${dataTestId ?? ''}notification#${id}`}
71
+ className={classNames(
72
+ styles.notification,
73
+ expandedContent && styles.notificationExpanded,
74
+ )}>
75
+ {isUnread ? (
76
+ <DotSvg
77
+ data-testid={`${dataTestId ?? ''}notification#${id}+unreadDot`}
78
+ className={styles.notificationUnreadDot}
79
+ width={8}
80
+ onClick={() => {
81
+ markNotificationAsRead(id);
82
+ }}
83
+ />
84
+ ) : (
85
+ <div className={styles.notificationReaded} />
86
+ )}
87
+ <NotificationContent
88
+ dataTestId={dataTestId}
89
+ id={id}
90
+ title={title}
91
+ content={content}
92
+ onClose={onClose}
93
+ date={date}
94
+ link={link}
95
+ handleNotificationClick={handleNotificationClick}
96
+ expandedContent={expandedContent}
97
+ toggleExpandContent={toggleExpandContent}
98
+ aggregatedSubtitle={aggregatedSubtitle}
99
+ aggregatedNotifications={aggregatedNotifications}
100
+ />
101
+ </div>
102
+ );
103
+ };
104
+
105
+ export default Notification;
@@ -0,0 +1,153 @@
1
+ import { CloseSvg } from '@mistertemp/design-system';
2
+ import { AggregatedNotificationMessage } from '@mistertemp/types-agency-notification-center';
3
+ import classNames from 'classnames';
4
+ import React, { FC } from 'react';
5
+
6
+ import { useBundledTranslation } from '../../hooks';
7
+ import es from '../../locales/es/notification.json';
8
+ import fr from '../../locales/fr/notification.json';
9
+ import it from '../../locales/it/notification.json';
10
+ import styles from './Notifications.module.scss';
11
+
12
+ const translationNs = 'notification';
13
+ const translations = { it, fr, es };
14
+
15
+ interface NotificationContentProps {
16
+ dataTestId?: string;
17
+ id: string;
18
+ title: string;
19
+ content: string;
20
+ onClose?: () => void;
21
+ date: string;
22
+ link: string;
23
+ handleNotificationClick: () => void;
24
+ expandedContent: boolean;
25
+ toggleExpandContent: () => void;
26
+ aggregatedSubtitle?: string;
27
+ aggregatedNotifications?: AggregatedNotificationMessage[];
28
+ }
29
+
30
+ export const NotificationContent: FC<NotificationContentProps> = ({
31
+ dataTestId = '',
32
+ id,
33
+ title,
34
+ content,
35
+ onClose,
36
+ date,
37
+ link,
38
+ handleNotificationClick,
39
+ expandedContent,
40
+ toggleExpandContent,
41
+ aggregatedSubtitle,
42
+ aggregatedNotifications,
43
+ }) => {
44
+ const isAggregated = aggregatedSubtitle || aggregatedNotifications;
45
+ const { t } = useBundledTranslation(translationNs, translations);
46
+
47
+ const handleExpandContentKeyDown = (e: React.KeyboardEvent<HTMLSpanElement>) => {
48
+ if (e.key === 'Enter' || e.key === ' ') {
49
+ toggleExpandContent();
50
+ }
51
+ };
52
+
53
+ const handleLinkClick = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
54
+ const target = e.target as Element;
55
+ const targetLink = target.getAttribute('href');
56
+
57
+ if (targetLink && targetLink.length < 2) {
58
+ e.preventDefault();
59
+ }
60
+
61
+ handleNotificationClick();
62
+ };
63
+
64
+ const NotificationHeader = (
65
+ <span
66
+ data-testid={`${dataTestId}notification#${id}+title`}
67
+ className={styles.notificationTitle}>
68
+ <span>
69
+ {title}
70
+ <span
71
+ data-testid={`${dataTestId}notification#${id}+date`}
72
+ className={styles.notificationDate}>
73
+ {date}
74
+ </span>
75
+ </span>
76
+ {onClose && (
77
+ <span>
78
+ <CloseSvg className={styles.notificationClose} onClick={onClose} />
79
+ </span>
80
+ )}
81
+ </span>
82
+ );
83
+
84
+ const NotificationDetails = (
85
+ <>
86
+ <span
87
+ data-testid={`${dataTestId}notification#${id}+content`}
88
+ className={classNames(
89
+ styles.notificationContent,
90
+ expandedContent && styles.notificationContentExpanded,
91
+ )}>
92
+ <span
93
+ className={styles.notificationMessage}
94
+ dangerouslySetInnerHTML={{ __html: content }}
95
+ />
96
+ <div className={styles.notificationAggregatedContent}>
97
+ <span>
98
+ {aggregatedSubtitle}
99
+ {aggregatedNotifications?.map((notif, i) => (
100
+ <a
101
+ data-testid={`${dataTestId}notification#${id}+aggregatedLink+${i}`}
102
+ key={`${dataTestId}notification#${id}+aggregatedLink+${i}`}
103
+ href={notif.link}
104
+ target="_blank"
105
+ rel="noreferrer"
106
+ onClick={handleLinkClick}
107
+ dangerouslySetInnerHTML={{ __html: notif.message }}
108
+ />
109
+ ))}
110
+ </span>
111
+ </div>
112
+ </span>
113
+ {isAggregated && (
114
+ <span
115
+ data-testid={`${dataTestId}notification#${id}+seeMore`}
116
+ className={classNames(
117
+ styles.notificationSeeMore,
118
+ expandedContent && styles.notificationSeeMoreExpanded,
119
+ )}
120
+ onClick={toggleExpandContent}
121
+ role="button"
122
+ tabIndex={0}
123
+ onKeyDown={handleExpandContentKeyDown}>
124
+ {expandedContent
125
+ ? t('notification:notificationsPopover.showLess')
126
+ : t('notification:notificationsPopover.showMore')}
127
+ </span>
128
+ )}
129
+ </>
130
+ );
131
+
132
+ return !isAggregated ? (
133
+ <span>
134
+ {NotificationHeader}
135
+ <a
136
+ data-testid={`${dataTestId}notification#${id}+link`}
137
+ onClick={handleLinkClick}
138
+ href={link}
139
+ target="_blank"
140
+ className={styles.notificationLink}
141
+ rel="noreferrer">
142
+ {NotificationDetails}
143
+ </a>
144
+ </span>
145
+ ) : (
146
+ <div data-testid={`${dataTestId}notification#${id}+container`}>
147
+ {NotificationHeader}
148
+ {NotificationDetails}
149
+ </div>
150
+ );
151
+ };
152
+
153
+ export default NotificationContent;