@carlonicora/nextjs-jsonapi 1.0.4 → 1.0.6

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 (283) hide show
  1. package/LICENSE +675 -0
  2. package/dist/{AbstractService-BKlpJA61.d.mts → AbstractService-B2n_JdiC.d.mts} +1 -1
  3. package/dist/{AbstractService-D9eSVKNa.d.ts → AbstractService-DtQTYovo.d.ts} +1 -1
  4. package/dist/{content.interface-Dg2lt_An.d.mts → AuthComponent-CPLvEerw.d.mts} +11 -15
  5. package/dist/{content.interface-BhyAiOFq.d.ts → AuthComponent-m6Qp4Hz6.d.ts} +11 -15
  6. package/dist/{BlockNoteEditor-UVO3VZZE.mjs → BlockNoteEditor-BLVXQPXV.mjs} +14 -18
  7. package/dist/{BlockNoteEditor-UVO3VZZE.mjs.map → BlockNoteEditor-BLVXQPXV.mjs.map} +1 -1
  8. package/dist/{BlockNoteEditor-VFWG6LXI.js → BlockNoteEditor-ZTDHULFT.js} +15 -19
  9. package/dist/BlockNoteEditor-ZTDHULFT.js.map +1 -0
  10. package/dist/JsonApiRequest-O7BGUMFO.mjs +23 -0
  11. package/dist/JsonApiRequest-VARLNKAF.js +23 -0
  12. package/dist/JsonApiRequest-VARLNKAF.js.map +1 -0
  13. package/dist/chunk-2LM6LCJW.mjs +1 -0
  14. package/dist/chunk-3APORDYP.mjs +7783 -0
  15. package/dist/chunk-3APORDYP.mjs.map +1 -0
  16. package/dist/{chunk-TMVHSY3Y.js → chunk-5ZEADNNP.js} +36 -17
  17. package/dist/chunk-5ZEADNNP.js.map +1 -0
  18. package/dist/{chunk-ECDTZBYO.mjs → chunk-74F6BBHH.mjs} +21 -2
  19. package/dist/chunk-74F6BBHH.mjs.map +1 -0
  20. package/dist/{chunk-GYWPEPOH.mjs → chunk-7C5RAEBO.mjs} +72 -68
  21. package/dist/chunk-7C5RAEBO.mjs.map +1 -0
  22. package/dist/chunk-A5DDIABK.js +1 -0
  23. package/dist/{chunk-TEGF6ZWG.js → chunk-AGAJMJ4T.js} +47 -9
  24. package/dist/chunk-AGAJMJ4T.js.map +1 -0
  25. package/dist/{chunk-CXQOWQSY.js → chunk-AYHKQWHH.js} +15 -2
  26. package/dist/chunk-AYHKQWHH.js.map +1 -0
  27. package/dist/{chunk-I2REI7OA.js → chunk-HMHGLXWC.js} +33 -15
  28. package/dist/chunk-HMHGLXWC.js.map +1 -0
  29. package/dist/chunk-IWFGEPAA.mjs +1 -0
  30. package/dist/chunk-JC3WJK65.js +1 -0
  31. package/dist/{chunk-L6EQEAXU.mjs → chunk-PYF2U6WG.mjs} +25 -7
  32. package/dist/chunk-PYF2U6WG.mjs.map +1 -0
  33. package/dist/{chunk-YDVTFM7X.mjs → chunk-RBIVEH2K.mjs} +42 -4
  34. package/dist/chunk-RBIVEH2K.mjs.map +1 -0
  35. package/dist/{chunk-V2JJPI7N.js → chunk-RZO2LOW4.js} +237 -233
  36. package/dist/chunk-RZO2LOW4.js.map +1 -0
  37. package/dist/{chunk-X4BIHJ2B.mjs → chunk-SM63SZCP.mjs} +15 -2
  38. package/dist/chunk-SM63SZCP.mjs.map +1 -0
  39. package/dist/chunk-WEC4YMOS.js +7783 -0
  40. package/dist/chunk-WEC4YMOS.js.map +1 -0
  41. package/dist/client/index.d.mts +21 -2
  42. package/dist/client/index.d.ts +21 -2
  43. package/dist/client/index.js +18 -245
  44. package/dist/client/index.js.map +1 -1
  45. package/dist/client/index.mjs +28 -255
  46. package/dist/client/index.mjs.map +1 -1
  47. package/dist/components/index.d.mts +293 -8
  48. package/dist/components/index.d.ts +293 -8
  49. package/dist/components/index.js +78 -2323
  50. package/dist/components/index.js.map +1 -1
  51. package/dist/components/index.mjs +172 -2417
  52. package/dist/components/index.mjs.map +1 -1
  53. package/dist/{config-hXufftVS.d.ts → config-BmnK65TD.d.mts} +1 -0
  54. package/dist/{config-hXufftVS.d.mts → config-BmnK65TD.d.ts} +1 -0
  55. package/dist/config-DQeAo9Kf.d.mts +49 -0
  56. package/dist/config-DQeAo9Kf.d.ts +49 -0
  57. package/dist/contexts/index.d.mts +109 -21
  58. package/dist/contexts/index.d.ts +109 -21
  59. package/dist/contexts/index.js +39 -7
  60. package/dist/contexts/index.js.map +1 -1
  61. package/dist/contexts/index.mjs +40 -8
  62. package/dist/core/index.d.mts +3 -4
  63. package/dist/core/index.d.ts +3 -4
  64. package/dist/core/index.js +3 -7
  65. package/dist/core/index.js.map +1 -1
  66. package/dist/core/index.mjs +4 -8
  67. package/dist/{d3.link.interface-QMdB22bC.d.mts → d3.link.interface-ClC4Irqp.d.mts} +2 -1
  68. package/dist/{d3.link.interface-QMdB22bC.d.ts → d3.link.interface-ClC4Irqp.d.ts} +2 -1
  69. package/dist/features/index.d.mts +17 -86
  70. package/dist/features/index.d.ts +17 -86
  71. package/dist/features/index.js +7 -16
  72. package/dist/features/index.js.map +1 -1
  73. package/dist/features/index.mjs +10 -19
  74. package/dist/hooks/index.d.mts +18 -43
  75. package/dist/hooks/index.d.ts +18 -43
  76. package/dist/hooks/index.js +20 -7
  77. package/dist/hooks/index.js.map +1 -1
  78. package/dist/hooks/index.mjs +19 -6
  79. package/dist/index.d.mts +10 -6
  80. package/dist/index.d.ts +10 -6
  81. package/dist/index.js +13 -10
  82. package/dist/index.js.map +1 -1
  83. package/dist/index.mjs +22 -19
  84. package/dist/interfaces/index.d.mts +2 -1
  85. package/dist/interfaces/index.d.ts +2 -1
  86. package/dist/notification.interface-BBgMUdLR.d.mts +14 -0
  87. package/dist/notification.interface-gyvT-Z2F.d.ts +14 -0
  88. package/dist/permissions/index.d.mts +2 -3
  89. package/dist/permissions/index.d.ts +2 -3
  90. package/dist/server/index.d.mts +38 -18
  91. package/dist/server/index.d.ts +38 -18
  92. package/dist/server/index.js +70 -2
  93. package/dist/server/index.js.map +1 -1
  94. package/dist/server/index.mjs +68 -0
  95. package/dist/server/index.mjs.map +1 -1
  96. package/dist/types-BUAlgqqh.d.ts +39 -0
  97. package/dist/{types-DluCaP1I.d.ts → types-Bl61ob-7.d.mts} +19 -2
  98. package/dist/{types-lQVA8d_P.d.mts → types-Bl61ob-7.d.ts} +19 -2
  99. package/dist/types-iVdVY7ba.d.mts +39 -0
  100. package/dist/useSocket-Cn7fB_B1.d.mts +25 -0
  101. package/dist/useSocket-DzMKRKCA.d.ts +25 -0
  102. package/dist/user.fields-CbdObSmS.d.mts +18 -0
  103. package/dist/user.fields-CbdObSmS.d.ts +18 -0
  104. package/dist/utils/index.d.mts +1 -2
  105. package/dist/utils/index.d.ts +1 -2
  106. package/package.json +5 -3
  107. package/src/client/index.ts +13 -0
  108. package/src/components/forms/index.ts +1 -0
  109. package/src/components/index.ts +5 -0
  110. package/src/components/tables/ContentListTable.tsx +1 -0
  111. package/src/contexts/CommonContext.tsx +52 -0
  112. package/src/contexts/SharedContext.tsx +2 -0
  113. package/src/contexts/SocketContext.tsx +65 -0
  114. package/src/contexts/index.ts +6 -1
  115. package/src/features/auth/components/containers/AuthContainer.tsx +32 -0
  116. package/src/features/auth/components/containers/index.ts +1 -0
  117. package/src/features/auth/components/details/LandingComponent.tsx +39 -0
  118. package/src/features/auth/components/details/index.ts +1 -0
  119. package/src/features/auth/components/forms/AcceptInvitation.tsx +136 -0
  120. package/src/features/auth/components/forms/ActivateAccount.tsx +75 -0
  121. package/src/features/auth/components/forms/Cookies.tsx +32 -0
  122. package/src/features/auth/components/forms/ForgotPassword.tsx +108 -0
  123. package/src/features/auth/components/forms/Login.tsx +118 -0
  124. package/src/features/auth/components/forms/Logout.tsx +19 -0
  125. package/src/features/auth/components/forms/RefreshUser.tsx +39 -0
  126. package/src/features/auth/components/forms/Register.tsx +150 -0
  127. package/src/features/auth/components/forms/ResetPassword.tsx +126 -0
  128. package/src/features/auth/components/forms/index.ts +9 -0
  129. package/src/features/auth/components/index.ts +3 -0
  130. package/src/features/auth/contexts/AuthContext.tsx +77 -0
  131. package/src/features/auth/contexts/index.ts +1 -0
  132. package/src/features/auth/enums/AuthComponent.ts +9 -0
  133. package/src/features/auth/enums/index.ts +1 -0
  134. package/src/features/auth/index.ts +2 -1
  135. package/src/features/auth/utils/AuthCookies.ts +134 -0
  136. package/src/features/auth/utils/index.ts +1 -0
  137. package/src/features/company/components/containers/AdminCompanyContainer.tsx +26 -0
  138. package/src/features/company/components/containers/CompanyContainer.tsx +17 -0
  139. package/src/features/company/components/containers/index.ts +2 -0
  140. package/src/features/company/components/details/CompanyDetails.tsx +26 -0
  141. package/src/features/company/components/details/index.ts +1 -0
  142. package/src/features/company/components/forms/CompanyConfigurationEditor.tsx +151 -0
  143. package/src/features/company/components/forms/CompanyConfigurationSecurityForm.tsx +97 -0
  144. package/src/features/company/components/forms/CompanyDeleter.tsx +121 -0
  145. package/src/features/company/components/forms/CompanyEditor.tsx +245 -0
  146. package/src/features/company/components/forms/CompanyLicense.tsx +213 -0
  147. package/src/features/company/components/forms/index.ts +5 -0
  148. package/src/features/company/components/index.ts +4 -0
  149. package/src/features/company/components/lists/CompaniesList.tsx +31 -0
  150. package/src/features/company/components/lists/index.ts +1 -0
  151. package/src/features/company/contexts/CompanyContext.tsx +99 -0
  152. package/src/features/company/contexts/index.ts +0 -0
  153. package/src/features/company/hooks/index.ts +1 -0
  154. package/src/features/company/hooks/useCompanyTableStructure.tsx +82 -0
  155. package/src/features/feature/components/forms/FormFeatures.tsx +141 -140
  156. package/src/features/feature/components/forms/index.ts +1 -0
  157. package/src/features/feature/components/index.ts +1 -1
  158. package/src/features/feature/index.ts +1 -2
  159. package/src/features/module/index.ts +1 -1
  160. package/src/features/notification/components/common/NotificationErrorBoundary.tsx +51 -0
  161. package/src/features/notification/components/common/index.ts +1 -0
  162. package/src/features/notification/components/containers/NotificationsListContainer.tsx +44 -0
  163. package/src/features/notification/components/containers/index.ts +1 -0
  164. package/src/features/notification/components/index.ts +5 -0
  165. package/src/features/notification/components/lists/NotificationsList.tsx +129 -0
  166. package/src/features/notification/components/lists/index.ts +1 -0
  167. package/src/features/notification/components/modals/NotificationModal.tsx +220 -0
  168. package/src/features/notification/components/modals/index.ts +1 -0
  169. package/src/features/notification/components/notifications/Notification.tsx +120 -0
  170. package/src/features/notification/components/notifications/PushNotificationProvider.tsx +9 -0
  171. package/src/features/notification/components/notifications/index.ts +2 -0
  172. package/src/features/notification/contexts/NotificationContext.tsx +187 -0
  173. package/src/features/notification/contexts/index.ts +1 -0
  174. package/src/features/notification/index.ts +1 -1
  175. package/src/features/push/index.ts +1 -1
  176. package/src/features/role/components/containers/RoleContainer.tsx +18 -0
  177. package/src/features/role/components/containers/index.ts +1 -0
  178. package/src/features/role/components/details/RoleDetails.tsx +21 -0
  179. package/src/features/role/components/details/index.ts +1 -0
  180. package/src/features/role/components/forms/FormRoles.tsx +82 -0
  181. package/src/features/role/components/forms/RemoveUserFromRole.tsx +108 -0
  182. package/src/features/role/components/forms/UserRoleAdd.tsx +128 -0
  183. package/src/features/role/components/forms/index.ts +3 -0
  184. package/src/features/role/components/index.ts +4 -0
  185. package/src/features/role/components/lists/RolesList.tsx +27 -0
  186. package/src/features/role/components/lists/UserRolesList.tsx +31 -0
  187. package/src/features/role/components/lists/index.ts +2 -0
  188. package/src/features/role/contexts/RoleContext.tsx +84 -0
  189. package/src/features/role/contexts/index.ts +1 -0
  190. package/src/features/role/hooks/index.ts +1 -0
  191. package/src/features/role/hooks/useRoleTableStructure.tsx +72 -0
  192. package/src/features/s3/index.ts +1 -1
  193. package/src/features/user/components/containers/UserContainer.tsx +23 -0
  194. package/src/features/user/components/containers/UserIndexContainer.tsx +12 -0
  195. package/src/features/user/components/containers/UsersListContainer.tsx +36 -0
  196. package/src/features/user/components/containers/index.ts +3 -0
  197. package/src/features/user/components/details/UserDetails.tsx +74 -0
  198. package/src/features/user/components/details/UserIndexDetails.tsx +28 -0
  199. package/src/features/user/components/details/index.ts +2 -0
  200. package/src/features/user/components/forms/RoleUserAdd.tsx +93 -0
  201. package/src/features/user/components/forms/UserAvatarEditor.tsx +78 -0
  202. package/src/features/user/components/forms/UserDeleter.tsx +49 -0
  203. package/src/features/user/components/forms/UserEditor.tsx +319 -0
  204. package/src/features/user/components/forms/UserMultiSelect.tsx +218 -0
  205. package/src/features/user/components/forms/UserReactivator.tsx +79 -0
  206. package/src/features/user/components/forms/UserResentInvitationEmail.tsx +88 -0
  207. package/src/features/user/components/forms/UserSelector.tsx +185 -0
  208. package/src/features/user/components/forms/index.ts +8 -0
  209. package/src/features/user/components/index.ts +3 -0
  210. package/src/features/user/components/lists/AdminUsersList.tsx +41 -0
  211. package/src/features/user/components/lists/CompanyUsersList.tsx +44 -0
  212. package/src/features/user/components/lists/RelevantUsersList.tsx +30 -0
  213. package/src/features/user/components/lists/RoleUsersList.tsx +31 -0
  214. package/src/features/user/components/lists/UserListInAdd.tsx +53 -0
  215. package/src/features/user/components/lists/UsersList.tsx +30 -0
  216. package/src/features/user/components/lists/UsersListByContentIds.tsx +30 -0
  217. package/src/features/user/components/lists/index.ts +7 -0
  218. package/src/features/user/components/widgets/UserAvatarList.tsx +31 -0
  219. package/src/features/user/components/widgets/UserSearchPopover.tsx +89 -0
  220. package/src/features/user/contexts/UserContext.tsx +106 -0
  221. package/src/features/user/contexts/index.ts +1 -0
  222. package/src/features/user/hooks/index.ts +2 -0
  223. package/src/features/user/hooks/useUserSearch.ts +53 -0
  224. package/src/features/user/hooks/useUserTableStructure.tsx +115 -0
  225. package/src/features/user/index.ts +0 -1
  226. package/src/hooks/index.ts +4 -0
  227. package/src/hooks/useCustomD3Graph.tsx +2 -0
  228. package/src/hooks/useNotificationSync.ts +20 -0
  229. package/src/hooks/usePageTracker.ts +69 -0
  230. package/src/hooks/usePushNotifications.ts +82 -0
  231. package/src/hooks/useSocket.ts +201 -0
  232. package/src/hooks/useTableGenerator.ts +6 -2
  233. package/src/i18n/config.ts +1 -0
  234. package/src/index.ts +4 -0
  235. package/src/interfaces/d3.link.interface.ts +2 -1
  236. package/src/server/ServerSession.ts +103 -0
  237. package/src/server/index.ts +2 -1
  238. package/src/unified/JsonApiRequest.ts +23 -0
  239. package/dist/ApiRequestDataTypeInterface-CUKFDBx2.d.mts +0 -20
  240. package/dist/ApiRequestDataTypeInterface-CUKFDBx2.d.ts +0 -20
  241. package/dist/BlockNoteEditor-VFWG6LXI.js.map +0 -1
  242. package/dist/JsonApiRequest-S3ICLM7B.mjs +0 -20
  243. package/dist/JsonApiRequest-ZZLSP26T.js +0 -20
  244. package/dist/JsonApiRequest-ZZLSP26T.js.map +0 -1
  245. package/dist/chunk-366S2JCC.mjs +0 -31
  246. package/dist/chunk-366S2JCC.mjs.map +0 -1
  247. package/dist/chunk-5W6AKZE6.mjs +0 -131
  248. package/dist/chunk-5W6AKZE6.mjs.map +0 -1
  249. package/dist/chunk-A3J3AAYM.mjs +0 -97
  250. package/dist/chunk-A3J3AAYM.mjs.map +0 -1
  251. package/dist/chunk-AWONBQQP.js +0 -97
  252. package/dist/chunk-AWONBQQP.js.map +0 -1
  253. package/dist/chunk-CXQOWQSY.js.map +0 -1
  254. package/dist/chunk-DKKMWBP4.mjs +0 -1
  255. package/dist/chunk-DKKMWBP4.mjs.map +0 -1
  256. package/dist/chunk-DO2HLAZO.js +0 -48
  257. package/dist/chunk-DO2HLAZO.js.map +0 -1
  258. package/dist/chunk-DZXDB3K2.mjs +0 -17
  259. package/dist/chunk-DZXDB3K2.mjs.map +0 -1
  260. package/dist/chunk-ECDTZBYO.mjs.map +0 -1
  261. package/dist/chunk-FY4SXJGU.js +0 -806
  262. package/dist/chunk-FY4SXJGU.js.map +0 -1
  263. package/dist/chunk-GYWPEPOH.mjs.map +0 -1
  264. package/dist/chunk-H6FMOA6B.js +0 -1
  265. package/dist/chunk-H6FMOA6B.js.map +0 -1
  266. package/dist/chunk-I2REI7OA.js.map +0 -1
  267. package/dist/chunk-J4Q36PMP.js +0 -31
  268. package/dist/chunk-J4Q36PMP.js.map +0 -1
  269. package/dist/chunk-L6EQEAXU.mjs.map +0 -1
  270. package/dist/chunk-MFO27OHB.mjs +0 -48
  271. package/dist/chunk-MFO27OHB.mjs.map +0 -1
  272. package/dist/chunk-RAF7PNLG.js +0 -131
  273. package/dist/chunk-RAF7PNLG.js.map +0 -1
  274. package/dist/chunk-RUR22SVM.js +0 -17
  275. package/dist/chunk-RUR22SVM.js.map +0 -1
  276. package/dist/chunk-TEGF6ZWG.js.map +0 -1
  277. package/dist/chunk-TMVHSY3Y.js.map +0 -1
  278. package/dist/chunk-V2JJPI7N.js.map +0 -1
  279. package/dist/chunk-WWWMJZEF.mjs +0 -806
  280. package/dist/chunk-WWWMJZEF.mjs.map +0 -1
  281. package/dist/chunk-X4BIHJ2B.mjs.map +0 -1
  282. package/dist/chunk-YDVTFM7X.mjs.map +0 -1
  283. /package/dist/{JsonApiRequest-S3ICLM7B.mjs.map → JsonApiRequest-O7BGUMFO.mjs.map} +0 -0
@@ -0,0 +1,129 @@
1
+ "use client";
2
+
3
+ import { ArchiveIcon } from "lucide-react";
4
+ import { useTranslations } from "next-intl";
5
+ import { Modules } from "../../../../core";
6
+ import { DataListRetriever, useDataListRetriever, usePageUrlGenerator } from "../../../../hooks";
7
+ import {
8
+ Button,
9
+ Card,
10
+ CardContent,
11
+ Link,
12
+ Skeleton,
13
+ Tooltip,
14
+ TooltipContent,
15
+ TooltipTrigger,
16
+ } from "../../../../shadcnui";
17
+ import { UserAvatar } from "../../../user/components";
18
+ import { NotificationInterface, NotificationService } from "../../data";
19
+ import { generateNotificationData } from "../notifications/Notification";
20
+
21
+ type NotificationsListProps = {
22
+ archived: boolean;
23
+ };
24
+
25
+ export function NotificationsList({ archived }: NotificationsListProps) {
26
+ const t = useTranslations();
27
+ const generateUrl = usePageUrlGenerator();
28
+
29
+ const data: DataListRetriever<NotificationInterface> = useDataListRetriever({
30
+ retriever: (params) => NotificationService.findMany(params),
31
+ retrieverParams: { isArchived: archived },
32
+ module: Modules.Notification,
33
+ });
34
+
35
+ const archiveNotification = async (notification: NotificationInterface) => {
36
+ await NotificationService.archive({ id: notification.id });
37
+ data.removeElement(notification);
38
+ };
39
+
40
+ const LoadingSkeleton = () => (
41
+ <div className="space-y-4">
42
+ {Array.from({ length: 3 }).map((_, i) => (
43
+ <Card key={i}>
44
+ <CardContent className="p-2">
45
+ <div className="flex w-full flex-row items-center">
46
+ <Skeleton className="mr-4 h-8 w-8 rounded-full" />
47
+ <div className="flex-1 space-y-2">
48
+ <Skeleton className="h-4 w-3/4" />
49
+ <Skeleton className="h-3 w-1/2" />
50
+ </div>
51
+ <Skeleton className="h-8 w-20" />
52
+ </div>
53
+ </CardContent>
54
+ </Card>
55
+ ))}
56
+ </div>
57
+ );
58
+
59
+ return (
60
+ <div className="space-y-4">
61
+ {data.isLoaded ? (
62
+ (data.data as NotificationInterface[])?.map((notification: NotificationInterface) => {
63
+ const notificationData = generateNotificationData({ notification: notification, generateUrl: generateUrl });
64
+
65
+ return (
66
+ <Card key={notification.id}>
67
+ <CardContent className="p-0">
68
+ <div className={`flex w-full flex-row items-center p-2`}>
69
+ {notificationData.actor ? (
70
+ <div className="flex w-12 max-w-12 px-2">
71
+ <Link href={generateUrl({ page: Modules.User, id: notificationData.actor.id })}>
72
+ <UserAvatar user={notificationData.actor} className="h-8 w-8" />
73
+ </Link>
74
+ </div>
75
+ ) : (
76
+ <div className="flex w-14 max-w-14 px-2"></div>
77
+ )}
78
+ <div className="flex w-full flex-col">
79
+ <p className="text-sm">
80
+ {t.rich(`foundations.notification.${notification.notificationType}.description` as any, {
81
+ strong: (chunks: any) => <strong>{chunks}</strong>,
82
+ actor: notificationData.actor?.name ?? "",
83
+ title: notificationData.title,
84
+ })}
85
+ </p>
86
+ <div className="text-muted-foreground mt-1 w-full text-xs">
87
+ {new Date(notification.createdAt).toLocaleString()}
88
+ </div>
89
+ </div>
90
+ <div className="flex flex-row items-center">
91
+ {notificationData.url ? (
92
+ <Link href={notificationData.url}>
93
+ <Button variant={`outline`} size={`sm`} onClick={(e) => e.stopPropagation()}>
94
+ {t(`foundations.notification.${notification.notificationType}.buttons.action` as any)}
95
+ </Button>
96
+ </Link>
97
+ ) : (
98
+ <></>
99
+ )}
100
+ {!archived && (
101
+ <Tooltip>
102
+ <TooltipTrigger asChild>
103
+ <Button
104
+ variant={`link`}
105
+ onClick={(e) => {
106
+ e.preventDefault();
107
+ e.stopPropagation();
108
+ archiveNotification(notification);
109
+ }}
110
+ className="text-muted-foreground hover:text-destructive ml-2"
111
+ >
112
+ <ArchiveIcon className="h-4 w-4 cursor-pointer" />
113
+ </Button>
114
+ </TooltipTrigger>
115
+ <TooltipContent>{t(`foundations.notification.buttons.archive`)}</TooltipContent>
116
+ </Tooltip>
117
+ )}
118
+ </div>
119
+ </div>
120
+ </CardContent>
121
+ </Card>
122
+ );
123
+ })
124
+ ) : (
125
+ <LoadingSkeleton />
126
+ )}
127
+ </div>
128
+ );
129
+ }
@@ -0,0 +1 @@
1
+ export * from "./NotificationsList";
@@ -0,0 +1,220 @@
1
+ import { BellIcon } from "lucide-react";
2
+ import { useTranslations } from "next-intl";
3
+ import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import { toast } from "sonner";
5
+ import { useSocketContext } from "../../../../contexts";
6
+ import { usePageUrlGenerator } from "../../../../hooks";
7
+ import {
8
+ Card,
9
+ CardHeader,
10
+ CardTitle,
11
+ Popover,
12
+ PopoverContent,
13
+ PopoverTrigger,
14
+ ScrollArea,
15
+ Separator,
16
+ SidebarMenuButton,
17
+ } from "../../../../shadcnui";
18
+ import { useNotificationContext } from "../../contexts/NotificationContext";
19
+ import { NotificationInterface } from "../../data";
20
+ import { NotificationErrorBoundary } from "../common";
21
+
22
+ interface NotificationModalProps {
23
+ isOpen: boolean;
24
+ setIsOpen: (open: boolean) => void;
25
+ }
26
+
27
+ function NotificationModalContent({ isOpen, setIsOpen }: NotificationModalProps) {
28
+ const instanceId = useRef(Math.random().toString(36).substr(2, 9));
29
+ const {
30
+ notifications,
31
+ addNotification,
32
+ generateNotification,
33
+ generateToastNotification,
34
+ markNotificationsAsRead,
35
+ isLoading,
36
+ error,
37
+ } = useNotificationContext();
38
+ const { socketNotifications, removeSocketNotification, clearSocketNotifications } = useSocketContext();
39
+ const t = useTranslations();
40
+ const generateUrl = usePageUrlGenerator();
41
+ const [newNotifications, setNewNotifications] = useState<boolean>(false);
42
+ const preventAutoClose = useRef(false);
43
+
44
+ const circuitBreakerRef = useRef({
45
+ count: 0,
46
+ resetTime: 0,
47
+ isOpen: false,
48
+ });
49
+
50
+ const checkCircuitBreaker = useCallback(() => {
51
+ const now = Date.now();
52
+ const breaker = circuitBreakerRef.current;
53
+
54
+ // Reset counter every 10 seconds
55
+ if (now > breaker.resetTime) {
56
+ breaker.count = 0;
57
+ breaker.resetTime = now + 10000; // 10 seconds
58
+ breaker.isOpen = false;
59
+ }
60
+
61
+ // Trip breaker if more than 20 notifications in 10 seconds
62
+ breaker.count++;
63
+ if (breaker.count > 20) {
64
+ breaker.isOpen = true;
65
+ return false;
66
+ }
67
+
68
+ return !breaker.isOpen;
69
+ }, []);
70
+
71
+ const { unreadCount, unreadIds } = useMemo(() => {
72
+ const unreadNotifications = notifications.filter((notif) => !notif.isRead);
73
+ return {
74
+ unreadCount: unreadNotifications.length,
75
+ unreadIds: unreadNotifications.map((notif) => notif.id),
76
+ };
77
+ }, [notifications]);
78
+
79
+ useEffect(() => {
80
+ setNewNotifications(unreadCount > 0);
81
+ }, [unreadCount]);
82
+
83
+ const processSocketNotificationsRef = useRef<NodeJS.Timeout | null>(null);
84
+
85
+ const processSocketNotifications = useCallback(() => {
86
+ if (socketNotifications.length === 0) {
87
+ return;
88
+ }
89
+
90
+ if (!checkCircuitBreaker()) {
91
+ clearSocketNotifications(); // Still clear to prevent memory leaks
92
+ return;
93
+ }
94
+
95
+ const currentSocketNotifications = [...socketNotifications];
96
+ clearSocketNotifications();
97
+
98
+ // Process notifications in smaller batches to prevent UI freeze
99
+ const batchSize = 3;
100
+ const batches = [];
101
+ for (let i = 0; i < currentSocketNotifications.length; i += batchSize) {
102
+ batches.push(currentSocketNotifications.slice(i, i + batchSize));
103
+ }
104
+
105
+ batches.forEach((batch, batchIndex) => {
106
+ setTimeout(() => {
107
+ batch.forEach((notification) => {
108
+ addNotification(notification);
109
+ const toastNotification = generateToastNotification(notification, t, generateUrl);
110
+
111
+ toast.message(toastNotification.title, {
112
+ description: toastNotification.description,
113
+ action: toastNotification.action,
114
+ });
115
+ });
116
+
117
+ // Only set newNotifications on the last batch
118
+ if (batchIndex === batches.length - 1) {
119
+ setNewNotifications(true);
120
+ }
121
+ }, batchIndex * 100); // 100ms delay between batches
122
+ });
123
+ }, [
124
+ socketNotifications,
125
+ clearSocketNotifications,
126
+ addNotification,
127
+ generateToastNotification,
128
+ t,
129
+ generateUrl,
130
+ checkCircuitBreaker,
131
+ ]);
132
+
133
+ // 🔗 SOCKET: Throttled processing with 300ms delay
134
+ useEffect(() => {
135
+ if (processSocketNotificationsRef.current) {
136
+ clearTimeout(processSocketNotificationsRef.current);
137
+ }
138
+
139
+ processSocketNotificationsRef.current = setTimeout(() => {
140
+ processSocketNotifications();
141
+ }, 300); // 300ms throttle
142
+
143
+ return () => {
144
+ if (processSocketNotificationsRef.current) {
145
+ clearTimeout(processSocketNotificationsRef.current);
146
+ }
147
+ };
148
+ }, [processSocketNotifications]);
149
+
150
+ const handleOpenChange = (newlyRequestedOpenState: boolean) => {
151
+ if (!newlyRequestedOpenState && preventAutoClose.current) {
152
+ return;
153
+ }
154
+
155
+ setIsOpen(newlyRequestedOpenState);
156
+
157
+ if (newlyRequestedOpenState) {
158
+ preventAutoClose.current = true;
159
+
160
+ if (unreadIds.length > 0) {
161
+ markNotificationsAsRead(unreadIds)
162
+ .catch((error) => {
163
+ console.error("❌ [NotificationModal] Failed to mark notifications as read:", error);
164
+ })
165
+ .finally(() => {
166
+ preventAutoClose.current = false;
167
+ // Workaround: re-open if it was open before
168
+ setIsOpen(true);
169
+ });
170
+ } else {
171
+ preventAutoClose.current = false;
172
+ }
173
+ setNewNotifications(false);
174
+ }
175
+ };
176
+
177
+ const unreadNotifications = newNotifications && unreadCount > 0;
178
+
179
+ return (
180
+ <Popover open={isOpen} onOpenChange={handleOpenChange} data-testid={`sidebar-notification button`}>
181
+ <PopoverTrigger asChild>
182
+ <SidebarMenuButton className="text-muted-foreground h-6" disabled={isLoading}>
183
+ <BellIcon
184
+ className={`h-5 w-5 cursor-pointer ${unreadNotifications ? "text-destructive" : ""} ${isLoading ? "animate-pulse" : ""}`}
185
+ />
186
+ {t(`types.notifications`, { count: 2 })}
187
+ </SidebarMenuButton>
188
+ </PopoverTrigger>
189
+ <PopoverContent className="relative left-10 w-80 border-0 p-0 shadow-none">
190
+ <Card>
191
+ <CardHeader className="p-4">
192
+ <CardTitle>{t(`types.notifications`, { count: 2 })}</CardTitle>
193
+ {isLoading && <div className="text-muted-foreground text-xs">Loading...</div>}
194
+ {error && <div className="text-destructive text-xs">Error: {error}</div>}
195
+ </CardHeader>
196
+ <Separator />
197
+ <ScrollArea className="h-96">
198
+ {notifications.length > 0 ? (
199
+ notifications.map((notification: NotificationInterface) => (
200
+ <Fragment key={notification.id}>{generateNotification(notification, () => setIsOpen(false))}</Fragment>
201
+ ))
202
+ ) : (
203
+ <div className="p-4 text-center text-sm text-gray-500">
204
+ {t(`foundations.notification.empty`, { count: 2 })}
205
+ </div>
206
+ )}
207
+ </ScrollArea>
208
+ </Card>
209
+ </PopoverContent>
210
+ </Popover>
211
+ );
212
+ }
213
+
214
+ export function NotificationModal(props: NotificationModalProps) {
215
+ return (
216
+ <NotificationErrorBoundary>
217
+ <NotificationModalContent {...props} />
218
+ </NotificationErrorBoundary>
219
+ );
220
+ }
@@ -0,0 +1 @@
1
+ export * from "./NotificationModal";
@@ -0,0 +1,120 @@
1
+ "use client";
2
+
3
+ import { useTranslations } from "next-intl";
4
+ import { ReactElement, useEffect, useState } from "react";
5
+ import { usePageUrlGenerator } from "../../../../hooks";
6
+ import { Link } from "../../../../shadcnui";
7
+ import { UserInterface } from "../../../user";
8
+ import { UserAvatar } from "../../../user/components";
9
+ import { NotificationInterface } from "../../data";
10
+
11
+ type TaskCommentedOnProps = {
12
+ notification: NotificationInterface;
13
+ closePopover: () => void;
14
+ };
15
+
16
+ export const generateNotificationData = (params: {
17
+ notification: NotificationInterface;
18
+ generateUrl: any;
19
+ }): { title: string; actor?: UserInterface; url?: string; taskId?: string } => {
20
+ const response: any = {};
21
+
22
+ response.actor = params.notification.actor;
23
+
24
+ return response;
25
+ };
26
+
27
+ export function NotificationToast(
28
+ notification: NotificationInterface,
29
+ t: any,
30
+ generateUrl: any,
31
+ reouter: any,
32
+ ): {
33
+ title: string;
34
+ description: string | ReactElement<any>;
35
+ action?: {
36
+ label: string;
37
+ onClick: () => void;
38
+ };
39
+ } {
40
+ const data = generateNotificationData({ notification: notification, generateUrl: generateUrl });
41
+
42
+ return {
43
+ title: t(`foundations.notification.${notification.notificationType}.title`),
44
+ description: (
45
+ <div className={`flex w-full flex-row items-center p-2`}>
46
+ {data.actor ? (
47
+ <div className="flex w-12 max-w-12 px-2">
48
+ <UserAvatar user={data.actor} className="h-8 w-8" />
49
+ </div>
50
+ ) : (
51
+ <div className="flex w-14 max-w-14 px-2"></div>
52
+ )}
53
+ <div className="flex w-full flex-col">
54
+ <p className="text-sm">
55
+ {t.rich(`foundations.notification.${notification.notificationType}.description`, {
56
+ strong: (chunks: any) => <strong>{chunks}</strong>,
57
+ actor: data.actor?.name ?? "",
58
+ title: data.title,
59
+ })}
60
+ </p>
61
+ <div className="text-muted-foreground mt-1 w-full text-xs">
62
+ {new Date(notification.createdAt).toLocaleString()}
63
+ </div>
64
+ </div>
65
+ </div>
66
+ ),
67
+ action: data.url
68
+ ? {
69
+ label: t(`foundations.notification.${notification.notificationType}.buttons.action`),
70
+ onClick: () => {
71
+ reouter.push(data.url!);
72
+ },
73
+ }
74
+ : undefined,
75
+ };
76
+ }
77
+
78
+ export function NotificationMenuItem({ notification, closePopover }: TaskCommentedOnProps) {
79
+ const generateUrl = usePageUrlGenerator();
80
+ const [isRead, setIsRead] = useState<boolean>(false);
81
+ const t = useTranslations();
82
+
83
+ useEffect(() => {
84
+ setIsRead(notification.isRead);
85
+ }, []);
86
+
87
+ const data = generateNotificationData({ notification: notification, generateUrl: generateUrl });
88
+
89
+ const response = (
90
+ <div className={`flex w-full flex-row p-2 ${isRead ? "" : "bg-muted"} items-center`}>
91
+ {data.actor ? (
92
+ <div className="flex w-12 max-w-12 px-2">
93
+ <UserAvatar user={data.actor} className="h-8 w-8" />
94
+ </div>
95
+ ) : (
96
+ <div className="flex w-14 max-w-14 px-2"></div>
97
+ )}
98
+ <div className="flex w-full flex-col">
99
+ <p className="text-sm">
100
+ {t.rich(`foundations.notification.${notification.notificationType}.description` as any, {
101
+ strong: (chunks: any) => <strong>{chunks}</strong>,
102
+ actor: data.actor?.name ?? "",
103
+ title: data.title,
104
+ })}
105
+ </p>
106
+ <div className="text-muted-foreground mt-1 w-full text-xs">
107
+ {new Date(notification.createdAt).toLocaleString()}
108
+ </div>
109
+ </div>
110
+ </div>
111
+ );
112
+
113
+ if (!data.url) return response;
114
+
115
+ return (
116
+ <Link href={data.url} onClick={closePopover}>
117
+ {response}
118
+ </Link>
119
+ );
120
+ }
@@ -0,0 +1,9 @@
1
+ "use client";
2
+
3
+ import { ReactNode, type JSX } from "react";
4
+ import usePushNotifications from "../../../../hooks/usePushNotifications";
5
+
6
+ export function PushNotificationProvider({ children }: { children: ReactNode }): JSX.Element {
7
+ usePushNotifications();
8
+ return <>{children}</>;
9
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./Notification";
2
+ export * from "./PushNotificationProvider";
@@ -0,0 +1,187 @@
1
+ "use client";
2
+
3
+ import { useTranslations } from "next-intl";
4
+ import React, { createContext, ReactElement, useCallback, useContext, useState } from "react";
5
+ import { SharedProvider } from "../../../contexts";
6
+ import { Modules } from "../../../core";
7
+ import { useI18nRouter } from "../../../i18n";
8
+ import { BreadcrumbItemData } from "../../../interfaces";
9
+ import { NotificationMenuItem, NotificationToast } from "../components/notifications/Notification";
10
+ import { NotificationInterface, NotificationService } from "../data";
11
+
12
+ interface NotificationContextType {
13
+ notifications: NotificationInterface[];
14
+ setNotifications: (notifications: NotificationInterface[]) => void;
15
+ addNotification: (notification: NotificationInterface) => void;
16
+ addSocketNotifications: (socketNotifications: NotificationInterface[]) => void;
17
+ loadNotifications: () => Promise<void>;
18
+ generateNotification: (notification: NotificationInterface, closePopover: () => void) => ReactElement<any>;
19
+ generateToastNotification: (
20
+ notification: NotificationInterface,
21
+ t: any,
22
+ generateUrl: any,
23
+ ) => {
24
+ title: string;
25
+ description: string | ReactElement<any>;
26
+ action?: {
27
+ label: string;
28
+ onClick: () => void;
29
+ };
30
+ };
31
+ markNotificationsAsRead: (ids: string[]) => Promise<void>;
32
+ isLoading: boolean;
33
+ error: string | null;
34
+ lastLoaded: number;
35
+ shouldRefresh: boolean;
36
+ }
37
+
38
+ const NotificationContext = createContext<NotificationContextType | undefined>(undefined);
39
+
40
+ type NotificationContextProviderProps = {
41
+ children: React.ReactNode;
42
+ };
43
+
44
+ export const NotificationContextProvider = ({ children }: NotificationContextProviderProps) => {
45
+ const t = useTranslations();
46
+ const router = useI18nRouter();
47
+
48
+ const [notifications, setNotifications] = useState<NotificationInterface[]>([]);
49
+ const [isLoading, setIsLoading] = useState(false);
50
+ const [error, setError] = useState<string | null>(null);
51
+ const [lastLoaded, setLastLoaded] = useState(0);
52
+
53
+ // Calculate shouldRefresh (5 minute cache)
54
+ const shouldRefresh = Date.now() - lastLoaded > 5 * 60 * 1000;
55
+
56
+ const addNotification = useCallback((notification: NotificationInterface) => {
57
+ setNotifications((prev) => {
58
+ // Check if notification already exists to prevent duplicates
59
+ const exists = prev.some((n) => n.id === notification.id);
60
+ if (exists) return prev;
61
+ return [notification, ...prev];
62
+ });
63
+ }, []);
64
+
65
+ const addSocketNotifications = useCallback((socketNotifications: NotificationInterface[]) => {
66
+ setNotifications((prev) => {
67
+ const newNotifications = socketNotifications.filter(
68
+ (newNotif) => !prev.some((existingNotif) => existingNotif.id === newNotif.id),
69
+ );
70
+ return [...prev, ...newNotifications];
71
+ });
72
+ }, []);
73
+
74
+ const loadNotifications = useCallback(async () => {
75
+ setIsLoading(true);
76
+ setError(null);
77
+
78
+ try {
79
+ const fetchedNotifications = await NotificationService.findMany({});
80
+ setNotifications(fetchedNotifications);
81
+ setLastLoaded(Date.now());
82
+ } catch (error) {
83
+ const errorMessage = error instanceof Error ? error.message : "Failed to load notifications";
84
+ setError(errorMessage);
85
+ } finally {
86
+ setIsLoading(false);
87
+ }
88
+ }, []);
89
+
90
+ const markNotificationsAsRead = useCallback(async (ids: string[]) => {
91
+ setIsLoading(true);
92
+ setError(null);
93
+
94
+ try {
95
+ const data: any = {
96
+ data: ids.map((id: string) => ({
97
+ type: Modules.Notification.name,
98
+ id: id,
99
+ attributes: {
100
+ isRead: true,
101
+ },
102
+ meta: {},
103
+ relationships: {},
104
+ })),
105
+ };
106
+
107
+ await NotificationService.markAsRead({ data: data });
108
+ const allNotifications = await NotificationService.findMany({});
109
+ setNotifications(allNotifications);
110
+ setLastLoaded(Date.now());
111
+ } catch (error) {
112
+ const errorMessage = error instanceof Error ? error.message : "Failed to mark notifications as read";
113
+ setError(errorMessage);
114
+ throw error;
115
+ } finally {
116
+ setIsLoading(false);
117
+ }
118
+ }, []);
119
+
120
+ const generateToastNotification = (
121
+ notification: NotificationInterface,
122
+ t: any,
123
+ generateUrl: any,
124
+ ): {
125
+ title: string;
126
+ description: string | ReactElement<any>;
127
+ action?: {
128
+ label: string;
129
+ onClick: () => void;
130
+ };
131
+ } => {
132
+ return NotificationToast(notification, t, generateUrl, router);
133
+ };
134
+
135
+ const generateNotification = (notification: NotificationInterface, closePopover: () => void) => {
136
+ return <NotificationMenuItem notification={notification} closePopover={closePopover} />;
137
+ };
138
+
139
+ const breadcrumb = () => {
140
+ const response: BreadcrumbItemData[] = [];
141
+
142
+ response.push({
143
+ name: t(`types.notifications`, { count: 2 }),
144
+ });
145
+
146
+ return response;
147
+ };
148
+
149
+ const title = () => {
150
+ const response: any = {
151
+ type: t(`types.notifications`, { count: 2 }),
152
+ };
153
+
154
+ return response;
155
+ };
156
+
157
+ return (
158
+ <SharedProvider value={{ breadcrumbs: breadcrumb(), title: title() }}>
159
+ <NotificationContext.Provider
160
+ value={{
161
+ notifications,
162
+ setNotifications,
163
+ addNotification,
164
+ addSocketNotifications,
165
+ loadNotifications,
166
+ generateNotification,
167
+ generateToastNotification,
168
+ markNotificationsAsRead,
169
+ isLoading,
170
+ error,
171
+ lastLoaded,
172
+ shouldRefresh,
173
+ }}
174
+ >
175
+ {children}
176
+ </NotificationContext.Provider>
177
+ </SharedProvider>
178
+ );
179
+ };
180
+
181
+ export const useNotificationContext = (): NotificationContextType => {
182
+ const context = useContext(NotificationContext);
183
+ if (context === undefined) {
184
+ throw new Error(`Notification.messages.errors.use_context`);
185
+ }
186
+ return context;
187
+ };
@@ -0,0 +1 @@
1
+ export * from "./NotificationContext";
@@ -1,2 +1,2 @@
1
- export * from "./data";
2
1
  export * from "./notification.module";
2
+ export * from "./data";
@@ -1,2 +1,2 @@
1
- export * from "./data";
2
1
  export * from "./push.module";
2
+ export * from "./data";