@goplusvn/core 0.1.0 → 0.1.1

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 (369) hide show
  1. package/package.json +2 -1
  2. package/src/assets/erp_wallpaper.png +0 -0
  3. package/src/assets/goeat_logo.png +0 -0
  4. package/src/audit/audit-manager.ts +139 -0
  5. package/src/audit/index.ts +11 -0
  6. package/src/audit/memory-audit-logger.ts +86 -0
  7. package/src/audit/types.ts +50 -0
  8. package/src/auth/auth-service.ts +97 -0
  9. package/src/auth/index.ts +266 -0
  10. package/src/code-generation/index.ts +69 -0
  11. package/src/configs/auth-routes.ts +17 -0
  12. package/src/configs/crud.ts +136 -0
  13. package/src/configs/data/navigations.ts +781 -0
  14. package/src/configs/data/oauth-links.ts +10 -0
  15. package/src/configs/entities/material-categories.config.ts +125 -0
  16. package/src/configs/i18n.ts +12 -0
  17. package/src/configs/index.ts +26 -0
  18. package/src/configs/status.ts +25 -0
  19. package/src/configs/themes.ts +100 -0
  20. package/src/crud/components/crud-bulk-actions.tsx +91 -0
  21. package/src/crud/components/crud-card-view.tsx +241 -0
  22. package/src/crud/components/crud-context.tsx +122 -0
  23. package/src/crud/components/crud-delete-dialog.tsx +145 -0
  24. package/src/crud/components/crud-dialog.tsx +406 -0
  25. package/src/crud/components/crud-empty-state.tsx +104 -0
  26. package/src/crud/components/crud-export-button.tsx +170 -0
  27. package/src/crud/components/crud-field-renderer.tsx +653 -0
  28. package/src/crud/components/crud-filter-chips.tsx +102 -0
  29. package/src/crud/components/crud-filters/checkbox-filter.tsx +97 -0
  30. package/src/crud/components/crud-filters/datetime-filter.tsx +83 -0
  31. package/src/crud/components/crud-filters/filter-builder.tsx +66 -0
  32. package/src/crud/components/crud-filters/index.tsx +76 -0
  33. package/src/crud/components/crud-filters/radio-filter.tsx +86 -0
  34. package/src/crud/components/crud-filters/select-filter.tsx +141 -0
  35. package/src/crud/components/crud-filters/text-filter.tsx +86 -0
  36. package/src/crud/components/crud-form.tsx +642 -0
  37. package/src/crud/components/crud-import-dialog.tsx +440 -0
  38. package/src/crud/components/crud-infinite-scroll.tsx +116 -0
  39. package/src/crud/components/crud-page.tsx +1017 -0
  40. package/src/crud/components/crud-provider.tsx +277 -0
  41. package/src/crud/components/crud-row-actions.tsx +189 -0
  42. package/src/crud/components/crud-search.tsx +82 -0
  43. package/src/crud/components/crud-sheet.tsx +336 -0
  44. package/src/crud/components/crud-table-skeleton.tsx +26 -0
  45. package/src/crud/components/crud-table-toolbar.tsx +91 -0
  46. package/src/crud/components/crud-table.tsx +352 -0
  47. package/src/crud/components/crud-virtual-table.tsx +55 -0
  48. package/src/crud/components/index.tsx +20 -0
  49. package/src/crud/crud-filters/checkbox-filter.tsx +87 -0
  50. package/src/crud/crud-filters/datetime-filter.tsx +82 -0
  51. package/src/crud/crud-filters/filter-builder.tsx +64 -0
  52. package/src/crud/crud-filters/index.tsx +78 -0
  53. package/src/crud/crud-filters/radio-filter.tsx +79 -0
  54. package/src/crud/crud-filters/select-filter.tsx +148 -0
  55. package/src/crud/crud-filters/text-filter.tsx +81 -0
  56. package/src/crud/index.ts +43 -0
  57. package/src/crud/lib/crud-service.test.ts +334 -0
  58. package/src/crud/lib/crud-service.ts +358 -0
  59. package/src/crud/lib/crud-utils.test.ts +354 -0
  60. package/src/crud/lib/crud-utils.ts +299 -0
  61. package/src/crud/lib/crud-validator.ts +247 -0
  62. package/src/crud/lib/data-loader.ts +234 -0
  63. package/src/crud/lib/field-calculator.ts +241 -0
  64. package/src/crud/lib/field-formatter.ts +240 -0
  65. package/src/crud/lib/import-export-service.test.ts +290 -0
  66. package/src/crud/lib/import-export-service.ts +352 -0
  67. package/src/crud/lib/import-server-utils.ts +109 -0
  68. package/src/crud/lib/lazy-loader.ts +241 -0
  69. package/src/crud/lib/parse-filters.ts +85 -0
  70. package/src/crud/lib/permissions.ts +52 -0
  71. package/src/crud/lib/serialize-config.ts +60 -0
  72. package/src/crud/lib/stream-loader.ts +145 -0
  73. package/src/crud/lib/translate-config.ts +335 -0
  74. package/src/crud/lib/types.ts +11 -0
  75. package/src/crud/pages/entity-crud-page.tsx +144 -0
  76. package/src/crud/server.ts +8 -0
  77. package/src/home/constants.tsx +142 -0
  78. package/src/home/feature-showcase.tsx +171 -0
  79. package/src/home/home-page.tsx +191 -0
  80. package/src/home/hooks/index.ts +1 -0
  81. package/src/home/hooks/useWidgetPreferences.ts +167 -0
  82. package/src/home/index.ts +33 -0
  83. package/src/home/quick-access-dialog.tsx +271 -0
  84. package/src/home/quick-access-menu.tsx +267 -0
  85. package/src/home/types.ts +140 -0
  86. package/src/home/welcome-card.tsx +92 -0
  87. package/src/home/widget-container.tsx +258 -0
  88. package/src/home/widgets/base-widget.tsx +200 -0
  89. package/src/home/widgets/customers-widget.tsx +74 -0
  90. package/src/home/widgets/index.ts +6 -0
  91. package/src/home/widgets/orders-widget.tsx +87 -0
  92. package/src/home/widgets/revenue-widget.tsx +71 -0
  93. package/src/home/widgets/stock-widget.tsx +109 -0
  94. package/src/hooks/index.tsx +598 -0
  95. package/src/hooks/use-tenant.test.tsx +30 -0
  96. package/src/hooks/use-tenant.ts +5 -0
  97. package/src/index.ts +17 -0
  98. package/src/infrastructure/__tests__/architecture-verification.spec.ts +103 -0
  99. package/src/infrastructure/api-service.ts +317 -0
  100. package/src/infrastructure/cache/cache-manager.ts +107 -0
  101. package/src/infrastructure/cache/cache.ts +120 -0
  102. package/src/infrastructure/cache/index.ts +8 -0
  103. package/src/infrastructure/cache/types.ts +48 -0
  104. package/src/infrastructure/cron/cron-manager.ts +239 -0
  105. package/src/infrastructure/cron/index.ts +6 -0
  106. package/src/infrastructure/cron/types.ts +41 -0
  107. package/src/infrastructure/event-bus/event-bus.ts +145 -0
  108. package/src/infrastructure/event-bus/index.ts +2 -0
  109. package/src/infrastructure/event-bus/types.ts +22 -0
  110. package/src/infrastructure/index.ts +32 -0
  111. package/src/infrastructure/lock/decorators.ts +67 -0
  112. package/src/infrastructure/lock/index.ts +2 -0
  113. package/src/infrastructure/lock/lock-manager.ts +33 -0
  114. package/src/infrastructure/logger/index.ts +2 -0
  115. package/src/infrastructure/logger/logger.ts +96 -0
  116. package/src/infrastructure/logger/types.ts +25 -0
  117. package/src/layout/index.tsx +185 -0
  118. package/src/navigation/index.ts +91 -0
  119. package/src/notification/index.ts +14 -0
  120. package/src/notification/notification-service.ts +120 -0
  121. package/src/notification/storage/in-memory.ts +56 -0
  122. package/src/notification/storage/index.ts +1 -0
  123. package/src/notification/types.ts +51 -0
  124. package/src/organization/branch-service.ts +299 -0
  125. package/src/organization/branches.config.ts +154 -0
  126. package/src/organization/index.ts +5 -0
  127. package/src/plugin/apps-registry.ts +97 -0
  128. package/src/plugin/index.ts +5 -0
  129. package/src/plugin/types.ts +41 -0
  130. package/src/providers/index.tsx +109 -0
  131. package/src/providers/tenant-provider.tsx +45 -0
  132. package/src/rbac/components/roles/role-card.tsx +158 -0
  133. package/src/rbac/components/roles/role-stats-cards.tsx +29 -0
  134. package/src/rbac/components/roles/role-toolbar.tsx +123 -0
  135. package/src/rbac/hooks/use-role-operations.ts +159 -0
  136. package/src/rbac/hooks/use-roles-data.ts +59 -0
  137. package/src/rbac/index.ts +297 -0
  138. package/src/rbac/lib/permission-helpers.ts +63 -0
  139. package/src/rbac/pages/action-list-page.tsx +25 -0
  140. package/src/rbac/pages/resource-list-page.tsx +25 -0
  141. package/src/rbac/pages/role-list-page.tsx +378 -0
  142. package/src/rbac/permission-service.ts +140 -0
  143. package/src/rbac/permissions.ts +135 -0
  144. package/src/rbac/resource-service.ts +115 -0
  145. package/src/rbac/resource-validator.ts +119 -0
  146. package/src/rbac/role-service.ts +165 -0
  147. package/src/rbac/server.ts +16 -0
  148. package/src/rbac/types.ts +38 -0
  149. package/src/schemas/action.schema.ts +66 -0
  150. package/src/schemas/branch.schema.ts +52 -0
  151. package/src/schemas/coming-soon-schema.ts +9 -0
  152. package/src/schemas/company.schema.ts +44 -0
  153. package/src/schemas/forgot-passward-schema.ts +9 -0
  154. package/src/schemas/index.ts +30 -0
  155. package/src/schemas/material-category.schema.ts +43 -0
  156. package/src/schemas/material-pricing.schema.ts +74 -0
  157. package/src/schemas/material.schema.ts +76 -0
  158. package/src/schemas/materials.ts +52 -0
  159. package/src/schemas/new-passward-schema.ts +15 -0
  160. package/src/schemas/partner-company.schema.ts +149 -0
  161. package/src/schemas/register-schema.ts +36 -0
  162. package/src/schemas/resource.schema.ts +133 -0
  163. package/src/schemas/role.schema.ts +11 -0
  164. package/src/schemas/sign-in-schema.ts +24 -0
  165. package/src/schemas/supplier-pricing.schema.ts +15 -0
  166. package/src/schemas/supplier.schema.ts +120 -0
  167. package/src/schemas/system-category-group.schema.ts +67 -0
  168. package/src/schemas/system-category.schema.ts +77 -0
  169. package/src/schemas/system-config.schema.ts +118 -0
  170. package/src/schemas/uom.schema.ts +75 -0
  171. package/src/schemas/user-supplier.schema.ts +179 -0
  172. package/src/schemas/user.schema.ts +18 -0
  173. package/src/schemas/verify-email-schema.ts +9 -0
  174. package/src/schemas/warehouse.schema.ts +49 -0
  175. package/src/system/components/categories/category-list.tsx +529 -0
  176. package/src/system/components/categories/category-manager.tsx +89 -0
  177. package/src/system/components/categories/group-sidebar.tsx +308 -0
  178. package/src/system/components/settings/setting-dialogs.tsx +197 -0
  179. package/src/system/components/settings/setting-field.tsx +291 -0
  180. package/src/system/components/settings/setting-form-dialog.tsx +308 -0
  181. package/src/system/components/settings/settings-groups.ts +80 -0
  182. package/src/system/components/settings/settings-search.tsx +71 -0
  183. package/src/system/components/settings/settings-section.tsx +74 -0
  184. package/src/system/components/settings/settings-sidebar.tsx +81 -0
  185. package/src/system/constants.ts +3 -0
  186. package/src/system/index.ts +150 -0
  187. package/src/system/job-manager.ts +176 -0
  188. package/src/system/pages/components/categories/category-list.tsx +537 -0
  189. package/src/system/pages/components/categories/category-manager.tsx +90 -0
  190. package/src/system/pages/components/categories/group-sidebar.tsx +311 -0
  191. package/src/system/pages/components/settings/sales-rules-settings.tsx +222 -0
  192. package/src/system/pages/components/settings/setting-dialogs.tsx +197 -0
  193. package/src/system/pages/components/settings/setting-field.tsx +292 -0
  194. package/src/system/pages/components/settings/setting-form-dialog.tsx +308 -0
  195. package/src/system/pages/components/settings/settings-groups.ts +87 -0
  196. package/src/system/pages/components/settings/settings-page.tsx +372 -0
  197. package/src/system/pages/components/settings/settings-search.tsx +71 -0
  198. package/src/system/pages/components/settings/settings-section.tsx +74 -0
  199. package/src/system/pages/components/settings/settings-sidebar.tsx +81 -0
  200. package/src/system/pages/components/settings/system-settings.tsx +244 -0
  201. package/src/system/pages/system-category-page.tsx +15 -0
  202. package/src/system/pages/system-settings-page.tsx +380 -0
  203. package/src/system/schemas/system-category-group.schema.ts +46 -0
  204. package/src/system/schemas/system-category.schema.ts +56 -0
  205. package/src/system/services/settings-service.ts +127 -0
  206. package/src/system/services/system-category-service.ts +63 -0
  207. package/src/system/types.ts +45 -0
  208. package/src/types/index.ts +703 -0
  209. package/src/ui/auth/auth-layout.tsx +135 -0
  210. package/src/ui/auth/forgot-password-form.tsx +98 -0
  211. package/src/ui/auth/index.tsx +7 -0
  212. package/src/ui/auth/new-password-form.tsx +107 -0
  213. package/src/ui/auth/oauth-links.tsx +30 -0
  214. package/src/ui/auth/register-form.tsx +202 -0
  215. package/src/ui/auth/sign-in-form.tsx +238 -0
  216. package/src/ui/auth/verify-email-form.tsx +104 -0
  217. package/src/ui/crud/index.tsx +10 -0
  218. package/src/ui/data-display/accordion.tsx +65 -0
  219. package/src/ui/data-display/aspect-ratio.tsx +11 -0
  220. package/src/ui/data-display/avatar.tsx +163 -0
  221. package/src/ui/data-display/bento-grid.tsx +77 -0
  222. package/src/ui/data-display/carousel.tsx +249 -0
  223. package/src/ui/data-display/chart.tsx +363 -0
  224. package/src/ui/data-display/code-block-highlight.tsx +54 -0
  225. package/src/ui/data-display/collapsible.tsx +42 -0
  226. package/src/ui/data-display/compact-stat-bar.tsx +149 -0
  227. package/src/ui/data-display/data-table/data-table-context.tsx +255 -0
  228. package/src/ui/data-display/data-table/data-table-empty-state.tsx +133 -0
  229. package/src/ui/data-display/data-table/data-table-skeleton.tsx +145 -0
  230. package/src/ui/data-display/data-table/data-table-toolbar.tsx +353 -0
  231. package/src/ui/data-display/data-table/data-table.tsx +597 -0
  232. package/src/ui/data-display/data-table/index.ts +44 -0
  233. package/src/ui/data-display/data-table-column-header.tsx +75 -0
  234. package/src/ui/data-display/data-table-pagination.tsx +130 -0
  235. package/src/ui/data-display/data-table-view-options.tsx +59 -0
  236. package/src/ui/data-display/formatted-number-input.tsx +210 -0
  237. package/src/ui/data-display/highlight.tsx +20 -0
  238. package/src/ui/data-display/hover-card.tsx +48 -0
  239. package/src/ui/data-display/index.tsx +50 -0
  240. package/src/ui/data-display/iphone-15-pro.tsx +114 -0
  241. package/src/ui/data-display/kanban/index.ts +4 -0
  242. package/src/ui/data-display/kanban/kanban-board.tsx +192 -0
  243. package/src/ui/data-display/kanban/kanban-column.tsx +74 -0
  244. package/src/ui/data-display/kanban/kanban-item.tsx +50 -0
  245. package/src/ui/data-display/kanban/kanban-types.ts +21 -0
  246. package/src/ui/data-display/kpi-card.tsx +68 -0
  247. package/src/ui/data-display/media-grid.tsx +110 -0
  248. package/src/ui/data-display/safari.tsx +175 -0
  249. package/src/ui/data-display/show-more-text.tsx +55 -0
  250. package/src/ui/data-display/tabs.tsx +68 -0
  251. package/src/ui/data-display/timeline.tsx +256 -0
  252. package/src/ui/feedback/alert.tsx +60 -0
  253. package/src/ui/feedback/context-menu.tsx +245 -0
  254. package/src/ui/feedback/drawer.tsx +132 -0
  255. package/src/ui/feedback/error-dialog.tsx +273 -0
  256. package/src/ui/feedback/index.tsx +183 -0
  257. package/src/ui/feedback/progress.tsx +32 -0
  258. package/src/ui/feedback/sheet.tsx +148 -0
  259. package/src/ui/feedback/sonner.tsx +36 -0
  260. package/src/ui/forms/command.tsx +157 -0
  261. package/src/ui/forms/date-picker.tsx +73 -0
  262. package/src/ui/forms/date-range-picker.tsx +76 -0
  263. package/src/ui/forms/date-time-picker.tsx +109 -0
  264. package/src/ui/forms/editor/editor-menu-bar.tsx +394 -0
  265. package/src/ui/forms/editor/index.tsx +130 -0
  266. package/src/ui/forms/editor/multi-select-example.tsx +1234 -0
  267. package/src/ui/forms/emoji-picker.tsx +109 -0
  268. package/src/ui/forms/file-dropzone.tsx +169 -0
  269. package/src/ui/forms/file-thumbnail.tsx +29 -0
  270. package/src/ui/forms/index.tsx +201 -0
  271. package/src/ui/forms/input-file.tsx +99 -0
  272. package/src/ui/forms/input-group.tsx +46 -0
  273. package/src/ui/forms/input-otp.tsx +81 -0
  274. package/src/ui/forms/input-phone.tsx +172 -0
  275. package/src/ui/forms/input-spin.tsx +116 -0
  276. package/src/ui/forms/input-tags.tsx +219 -0
  277. package/src/ui/forms/input-time.tsx +42 -0
  278. package/src/ui/forms/multi-select.tsx +629 -0
  279. package/src/ui/forms/multiple-date-picker.tsx +74 -0
  280. package/src/ui/forms/radio-group.tsx +42 -0
  281. package/src/ui/forms/rating.tsx +158 -0
  282. package/src/ui/forms/time-picker.tsx +57 -0
  283. package/src/ui/index.tsx +17 -0
  284. package/src/ui/layout/animated-list.tsx +77 -0
  285. package/src/ui/layout/animated-sidebar.tsx +294 -0
  286. package/src/ui/layout/command-menu.tsx +355 -0
  287. package/src/ui/layout/customizer.tsx +324 -0
  288. package/src/ui/layout/footer.tsx +43 -0
  289. package/src/ui/layout/full-screen-toggle.tsx +52 -0
  290. package/src/ui/layout/header-breadcrumb.tsx +77 -0
  291. package/src/ui/layout/horizontal-layout-header.tsx +83 -0
  292. package/src/ui/layout/horizontal-layout.tsx +50 -0
  293. package/src/ui/layout/index.tsx +25 -0
  294. package/src/ui/layout/language-dropdown.tsx +103 -0
  295. package/src/ui/layout/logo.tsx +63 -0
  296. package/src/ui/layout/main-layout.tsx +57 -0
  297. package/src/ui/layout/mode-dropdown.tsx +58 -0
  298. package/src/ui/layout/notification-dropdown.tsx +127 -0
  299. package/src/ui/layout/page-tabs.tsx +306 -0
  300. package/src/ui/layout/route-cache.tsx +214 -0
  301. package/src/ui/layout/sidebar-group-icon-menu.tsx +195 -0
  302. package/src/ui/layout/sidebar.tsx +279 -0
  303. package/src/ui/layout/tab-content-cache.tsx +201 -0
  304. package/src/ui/layout/tab-navigation-provider.tsx +536 -0
  305. package/src/ui/layout/toggle-mobile-sidebar.tsx +33 -0
  306. package/src/ui/layout/top-bar-header-menubar.tsx +412 -0
  307. package/src/ui/layout/user-dropdown.tsx +188 -0
  308. package/src/ui/layout/vertical-layout-header.tsx +65 -0
  309. package/src/ui/layout/vertical-layout.tsx +47 -0
  310. package/src/ui/management/audit-log-page.tsx +209 -0
  311. package/src/ui/management/cache-management.tsx +349 -0
  312. package/src/ui/management/index.ts +3 -0
  313. package/src/ui/management/job-management.tsx +308 -0
  314. package/src/ui/pages/not-found.tsx +30 -0
  315. package/src/ui/primitives/badge.tsx +66 -0
  316. package/src/ui/primitives/breadcrumb.tsx +103 -0
  317. package/src/ui/primitives/button.tsx +129 -0
  318. package/src/ui/primitives/calendar.tsx +74 -0
  319. package/src/ui/primitives/card.tsx +86 -0
  320. package/src/ui/primitives/checkbox.tsx +31 -0
  321. package/src/ui/primitives/client.ts +30 -0
  322. package/src/ui/primitives/combobox.tsx +290 -0
  323. package/src/ui/primitives/dialog.tsx +121 -0
  324. package/src/ui/primitives/dropdown-menu.tsx +239 -0
  325. package/src/ui/primitives/dynamic-icon.tsx +24 -0
  326. package/src/ui/primitives/index.tsx +134 -0
  327. package/src/ui/primitives/input-number.tsx +131 -0
  328. package/src/ui/primitives/input.tsx +22 -0
  329. package/src/ui/primitives/keyboard.tsx +23 -0
  330. package/src/ui/primitives/label.tsx +24 -0
  331. package/src/ui/primitives/menubar.tsx +262 -0
  332. package/src/ui/primitives/navigation-menu.tsx +157 -0
  333. package/src/ui/primitives/pagination.tsx +118 -0
  334. package/src/ui/primitives/popover.tsx +56 -0
  335. package/src/ui/primitives/prefetch-link.tsx +60 -0
  336. package/src/ui/primitives/resizable.tsx +59 -0
  337. package/src/ui/primitives/scroll-area.tsx +63 -0
  338. package/src/ui/primitives/select.tsx +172 -0
  339. package/src/ui/primitives/separator.tsx +51 -0
  340. package/src/ui/primitives/sidebar.tsx +844 -0
  341. package/src/ui/primitives/slider.tsx +27 -0
  342. package/src/ui/primitives/status-badge.tsx +47 -0
  343. package/src/ui/primitives/sticky-layout.tsx +50 -0
  344. package/src/ui/primitives/switch.tsx +29 -0
  345. package/src/ui/primitives/table.tsx +116 -0
  346. package/src/ui/primitives/tabs.tsx +55 -0
  347. package/src/ui/primitives/toggle-group.tsx +70 -0
  348. package/src/ui/primitives/toggle.tsx +47 -0
  349. package/src/ui/primitives/tooltip.tsx +59 -0
  350. package/src/user/components/dangerous-zone.tsx +34 -0
  351. package/src/user/components/delete-account-form.tsx +40 -0
  352. package/src/user/components/index.ts +4 -0
  353. package/src/user/components/profile-info-form.tsx +390 -0
  354. package/src/user/components/profile-info.tsx +32 -0
  355. package/src/user/components/unified-profile-dialog.tsx +1019 -0
  356. package/src/user/components/user-stats.tsx +27 -0
  357. package/src/user/components/user-toolbar.tsx +137 -0
  358. package/src/user/components/users-card-view.tsx +253 -0
  359. package/src/user/index.ts +11 -0
  360. package/src/user/pages/user-list-page.tsx +234 -0
  361. package/src/user/pages/users-client-page.tsx +385 -0
  362. package/src/user/profile-page.tsx +19 -0
  363. package/src/user/schemas.ts +68 -0
  364. package/src/user/types.ts +34 -0
  365. package/src/user/user-service.ts +538 -0
  366. package/src/utils/index.ts +906 -0
  367. package/src/workflow/activity-timeline.tsx +412 -0
  368. package/src/workflow/approval-workflow.tsx +31 -0
  369. package/src/workflow/index.ts +2 -0
@@ -0,0 +1,127 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { Bell } from "lucide-react";
5
+
6
+ import type { DictionaryType } from "../../hooks";
7
+
8
+ import { cn, formatDistance, formatUnreadCount } from "../../utils";
9
+
10
+ import { Badge, Button, buttonVariants, Card, CardFooter } from "../index";
11
+ import {
12
+ Popover,
13
+ PopoverContent,
14
+ PopoverTrigger,
15
+ DynamicIcon,
16
+ ScrollArea,
17
+ Tooltip,
18
+ TooltipTrigger,
19
+ TooltipContent,
20
+ } from "../primitives/client";
21
+
22
+ interface NotificationItem {
23
+ id: string;
24
+ url: string;
25
+ iconName: string;
26
+ content: string;
27
+ date: string | Date;
28
+ isRead: boolean;
29
+ }
30
+
31
+ interface NotificationDropdownProps {
32
+ dictionary: DictionaryType;
33
+ notifications?: NotificationItem[];
34
+ unreadCount?: number;
35
+ }
36
+
37
+ export function NotificationDropdown({
38
+ dictionary,
39
+ notifications = [],
40
+ unreadCount = 0,
41
+ }: NotificationDropdownProps) {
42
+ const displayCount = formatUnreadCount(unreadCount);
43
+
44
+ return (
45
+ <Popover modal>
46
+ <Tooltip>
47
+ <PopoverTrigger asChild>
48
+ <TooltipTrigger asChild>
49
+ <Button
50
+ variant="ghost"
51
+ size="icon"
52
+ className="relative"
53
+ >
54
+ <Bell className="size-4" />
55
+ <span className="sr-only">Notification</span>
56
+ {!!unreadCount && (
57
+ <Badge
58
+ className="absolute -top-1 -end-1 h-4 max-w-8 flex justify-center"
59
+ aria-live="polite"
60
+ aria-atomic="true"
61
+ role="status"
62
+ aria-label={`${displayCount} unread`}
63
+ >
64
+ {displayCount}
65
+ </Badge>
66
+ )}
67
+ </Button>
68
+ </TooltipTrigger>
69
+ </PopoverTrigger>
70
+ <TooltipContent>Notifications</TooltipContent>
71
+ </Tooltip>
72
+ <PopoverContent className="w-[380px] p-0">
73
+ <Card className="border-0 shadow-none">
74
+ <div className="flex items-center justify-between border-b border-border p-3">
75
+ <h3 className="text-sm font-semibold">
76
+ {dictionary.navigation.notifications.notifications}
77
+ </h3>
78
+ <Button variant="link" className="text-primary h-auto p-0">
79
+ {dictionary.navigation.notifications.dismissAll}
80
+ </Button>
81
+ </div>
82
+ <ScrollArea className="max-h-[300px]">
83
+ <ul>
84
+ {notifications.map((notification) => (
85
+ <li key={notification.id}>
86
+ <Link
87
+ href={notification.url}
88
+ className="flex items-center gap-2 py-4 px-6 hover:bg-accent hover:text-accent-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring"
89
+ >
90
+ <Badge className="h-10 w-10 flex items-center justify-center">
91
+ <DynamicIcon
92
+ name={notification.iconName as any}
93
+ className="h-5 w-5"
94
+ />
95
+ </Badge>
96
+ <div className="flex-1 w-0">
97
+ <p className="text-sm break-all truncate">
98
+ {notification.content}
99
+ </p>
100
+ <p className="text-sm text-muted-foreground">
101
+ {formatDistance(notification.date)}
102
+ </p>
103
+ </div>
104
+ {!notification.isRead && (
105
+ <div className="h-2 w-2 rounded-full bg-primary" />
106
+ )}
107
+ </Link>
108
+ </li>
109
+ ))}
110
+ </ul>
111
+ </ScrollArea>
112
+ <CardFooter className="justify-center border-t border-border p-0 min-h-12 flex items-center">
113
+ <Link
114
+ href=""
115
+ className={cn(
116
+ buttonVariants({ variant: "link" }),
117
+ "text-primary text-center",
118
+ )}
119
+ >
120
+ {dictionary.navigation.notifications.seeAllNotifications}
121
+ </Link>
122
+ </CardFooter>
123
+ </Card>
124
+ </PopoverContent>
125
+ </Popover>
126
+ );
127
+ }
@@ -0,0 +1,306 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { X, MoreHorizontal, Loader2, Pin } from "lucide-react";
5
+ import { useParams, useRouter } from "next/navigation";
6
+ import type { DictionaryType } from "../../hooks";
7
+ import type { LocaleType } from "../../types";
8
+
9
+ import { cn } from "../../utils";
10
+ import { Button, Badge } from "../index";
11
+ import {
12
+ DynamicIcon,
13
+ ScrollArea,
14
+ ScrollBar,
15
+ ContextMenu,
16
+ ContextMenuContent,
17
+ ContextMenuItem,
18
+ ContextMenuSeparator,
19
+ ContextMenuTrigger,
20
+ } from "../primitives/client";
21
+
22
+ import { useTabNavigation } from "./tab-navigation-provider";
23
+ import { useRouteCache } from "./route-cache";
24
+
25
+ interface PageTabsProps {
26
+ dictionary?: DictionaryType;
27
+ className?: string;
28
+ variant?: "default" | "header";
29
+ }
30
+
31
+ export function PageTabs({
32
+ dictionary,
33
+ className,
34
+ variant = "default",
35
+ }: PageTabsProps) {
36
+ const {
37
+ tabs,
38
+ activeTabId,
39
+ removeTab,
40
+ setActiveTab,
41
+ removeOtherTabs,
42
+ removeTabsToRight,
43
+ clearTabs,
44
+ } = useTabNavigation();
45
+ const { reloadTab } = useRouteCache();
46
+ const params = useParams();
47
+ const router = useRouter();
48
+ const locale = params?.lang as LocaleType | undefined;
49
+ const [contextMenuTabId, setContextMenuTabId] = useState<string | null>(null);
50
+
51
+ // Don't render if no tabs
52
+ if (tabs.length === 0) {
53
+ return null;
54
+ }
55
+
56
+ const handleTabClick = (tabId: string) => {
57
+ setActiveTab(tabId);
58
+ };
59
+
60
+ const handleReloadTab = (path: string) => {
61
+ reloadTab(path);
62
+ router.refresh();
63
+ };
64
+
65
+ const handleCloseTab = (e: React.MouseEvent, tabId: string) => {
66
+ e.stopPropagation();
67
+ removeTab(tabId);
68
+ };
69
+
70
+ const handleContextMenu = (tabId: string) => {
71
+ setContextMenuTabId(tabId);
72
+ };
73
+
74
+ // Sort tabs: pinned first, then by creation time
75
+ const sortedTabs = [...tabs].sort((a, b) => {
76
+ if (a.isPinned && !b.isPinned) return -1;
77
+ if (!a.isPinned && b.isPinned) return 1;
78
+ return a.createdAt - b.createdAt;
79
+ });
80
+
81
+ const currentTabIndex = sortedTabs.findIndex(
82
+ (tab) => tab.id === contextMenuTabId,
83
+ );
84
+ const hasTabsToRight =
85
+ currentTabIndex >= 0 && currentTabIndex < sortedTabs.length - 1;
86
+ const hasOtherTabs = sortedTabs.length > 1;
87
+
88
+ // Generate unique gradient background for each tab based on path
89
+ const getTabBackgroundColor = (path: string, isActive: boolean) => {
90
+ if (isActive) return "bg-background";
91
+
92
+ // Generate consistent hash from path
93
+ const normalizedPath = path.replace(/^\/[a-z]{2}(\/|$)/, "/");
94
+ const hash = normalizedPath.split("").reduce((acc, char) => {
95
+ return (acc << 5) - acc + char.charCodeAt(0);
96
+ }, 0);
97
+
98
+ // Refined gradient palette - darker and more visible (using dark theme colors for both modes)
99
+ const gradients = [
100
+ "bg-gradient-to-r from-slate-700 to-slate-600",
101
+ "bg-gradient-to-r from-zinc-700 to-zinc-600",
102
+ "bg-gradient-to-r from-stone-700 to-stone-600",
103
+ "bg-gradient-to-r from-neutral-700 to-neutral-600",
104
+ "bg-gradient-to-r from-blue-700 to-blue-600",
105
+ "bg-gradient-to-r from-indigo-700 to-indigo-600",
106
+ "bg-gradient-to-r from-purple-700 to-purple-600",
107
+ "bg-gradient-to-r from-violet-700 to-violet-600",
108
+ "bg-gradient-to-r from-fuchsia-700 to-fuchsia-600",
109
+ "bg-gradient-to-r from-pink-700 to-pink-600",
110
+ "bg-gradient-to-r from-rose-700 to-rose-600",
111
+ "bg-gradient-to-r from-red-700 to-red-600",
112
+ "bg-gradient-to-r from-orange-700 to-orange-600",
113
+ "bg-gradient-to-r from-amber-700 to-amber-600",
114
+ "bg-gradient-to-r from-yellow-700 to-yellow-600",
115
+ "bg-gradient-to-r from-lime-700 to-lime-600",
116
+ "bg-gradient-to-r from-green-700 to-green-600",
117
+ "bg-gradient-to-r from-emerald-700 to-emerald-600",
118
+ "bg-gradient-to-r from-teal-700 to-teal-600",
119
+ "bg-gradient-to-r from-cyan-700 to-cyan-600",
120
+ "bg-gradient-to-r from-sky-700 to-sky-600",
121
+ ];
122
+
123
+ // Select gradient based on hash to ensure consistency
124
+ const gradientIndex = Math.abs(hash) % gradients.length;
125
+ return gradients[gradientIndex];
126
+ };
127
+
128
+ return (
129
+ <div className={cn("w-full", variant === "header" && "py-0", className)}>
130
+ <ScrollArea className={cn("w-full", variant === "header" && "h-8")}>
131
+ <div
132
+ className={cn(
133
+ "flex items-end gap-0 px-2 py-0.5",
134
+ variant === "header" && "items-center py-0",
135
+ )}
136
+ >
137
+ {sortedTabs.map((tab, index) => {
138
+ const isActive = tab.id === activeTabId;
139
+ const isLast = index === sortedTabs.length - 1;
140
+ return (
141
+ <ContextMenu
142
+ key={tab.id}
143
+ onOpenChange={(open) => !open && setContextMenuTabId(null)}
144
+ >
145
+ <ContextMenuTrigger asChild>
146
+ <div
147
+ role="button"
148
+ tabIndex={0}
149
+ onClick={() => handleTabClick(tab.id)}
150
+ onContextMenu={() => handleContextMenu(tab.id)}
151
+ onMouseEnter={() => {
152
+ // Prefetch route when hovering over tab
153
+ if (!isActive && tab.path) {
154
+ router.prefetch(tab.path);
155
+ }
156
+ }}
157
+ onKeyDown={(e) => {
158
+ if (e.key === "Enter" || e.key === " ") {
159
+ e.preventDefault();
160
+ handleTabClick(tab.id);
161
+ }
162
+ }}
163
+ className={cn(
164
+ "group relative flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium transition-all duration-200",
165
+ "border border-transparent",
166
+ "hover:brightness-105 cursor-pointer",
167
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
168
+ getTabBackgroundColor(tab.path, isActive),
169
+ variant === "default" &&
170
+ (isActive
171
+ ? "bg-background text-foreground border-b-2 border-primary shadow-sm shadow-primary/10 border-t border-x border-b-0 rounded-t-md -mb-px z-10"
172
+ : "text-white hover:text-white border-b border-white/10 rounded-t-md"),
173
+ variant === "header" &&
174
+ (isActive
175
+ ? "bg-background text-foreground border-b-2 border-primary shadow-sm shadow-primary/10 border-t border-x rounded-t-md z-10"
176
+ : "text-white hover:text-white border-b border-white/10 rounded-t-md"),
177
+ !isLast &&
178
+ !isActive &&
179
+ variant === "default" &&
180
+ "border-r border-white/10",
181
+ )}
182
+ >
183
+ {/* Tab number indicator (for keyboard shortcuts) */}
184
+ {index < 9 && (
185
+ <span
186
+ className={cn(
187
+ "text-[10px] font-bold shrink-0 w-3 text-center opacity-50",
188
+ isActive && "opacity-70 text-primary",
189
+ )}
190
+ >
191
+ {index + 1}
192
+ </span>
193
+ )}
194
+ {/* Icon or status indicator */}
195
+ <div className="flex items-center gap-1 shrink-0">
196
+ {tab.isPinned && (
197
+ <Pin className="h-3 w-3 text-primary fill-current shrink-0" />
198
+ )}
199
+ {tab.isLoading ? (
200
+ <Loader2 className="h-3 w-3 animate-spin text-primary shrink-0" />
201
+ ) : tab.hasUnsavedChanges ? (
202
+ <div className="h-1.5 w-1.5 rounded-full bg-warning shrink-0" />
203
+ ) : tab.iconName ? (
204
+ <DynamicIcon
205
+ name={tab.iconName}
206
+ className={cn(
207
+ "h-3.5 w-3.5 shrink-0 transition-colors",
208
+ isActive
209
+ ? "text-primary"
210
+ : "text-white/70 group-hover:text-white",
211
+ )}
212
+ />
213
+ ) : null}
214
+ </div>
215
+ <span
216
+ className={cn(
217
+ "truncate max-w-[150px]",
218
+ isActive ? "font-semibold" : "font-medium",
219
+ )}
220
+ >
221
+ {tab.title}
222
+ </span>
223
+ {tab.badge !== undefined && (
224
+ <Badge
225
+ variant={isActive ? "default" : "secondary"}
226
+ className={cn(
227
+ "h-4 min-w-4 px-1.5 text-[10px] font-semibold shrink-0",
228
+ isActive &&
229
+ "bg-primary/20 text-primary border-primary/30",
230
+ )}
231
+ >
232
+ {typeof tab.badge === "number" && tab.badge > 99
233
+ ? "99+"
234
+ : tab.badge}
235
+ </Badge>
236
+ )}
237
+ <Button
238
+ variant="ghost"
239
+ size="icon"
240
+ className={cn(
241
+ "h-4 w-4 ml-0.5 opacity-0 group-hover:opacity-100 transition-opacity shrink-0",
242
+ "hover:bg-destructive/10 hover:text-destructive",
243
+ isActive && "opacity-100",
244
+ )}
245
+ onClick={(e) => handleCloseTab(e, tab.id)}
246
+ aria-label={`Close ${tab.title}`}
247
+ >
248
+ <X className="h-2.5 w-2.5" />
249
+ </Button>
250
+ </div>
251
+ </ContextMenuTrigger>
252
+ <ContextMenuContent>
253
+ <ContextMenuItem onClick={() => handleReloadTab(tab.path)}>
254
+ Reload Tab
255
+ </ContextMenuItem>
256
+ <ContextMenuItem onClick={() => handleTabClick(tab.id)}>
257
+ Switch to Tab
258
+ </ContextMenuItem>
259
+ <ContextMenuItem onClick={() => removeTab(tab.id)}>
260
+ Close Tab
261
+ </ContextMenuItem>
262
+ {hasOtherTabs && (
263
+ <>
264
+ <ContextMenuSeparator />
265
+ <ContextMenuItem onClick={() => removeOtherTabs(tab.id)}>
266
+ Close Other Tabs
267
+ </ContextMenuItem>
268
+ {hasTabsToRight && (
269
+ <ContextMenuItem
270
+ onClick={() => removeTabsToRight(tab.id)}
271
+ >
272
+ Close Tabs to the Right
273
+ </ContextMenuItem>
274
+ )}
275
+ </>
276
+ )}
277
+ {tabs.length > 1 && (
278
+ <>
279
+ <ContextMenuSeparator />
280
+ <ContextMenuItem onClick={() => clearTabs()}>
281
+ Close All Tabs
282
+ </ContextMenuItem>
283
+ </>
284
+ )}
285
+ </ContextMenuContent>
286
+ </ContextMenu>
287
+ );
288
+ })}
289
+ {sortedTabs.length > 3 && (
290
+ <Button
291
+ variant="ghost"
292
+ size="sm"
293
+ className="h-6 px-2 text-xs ml-1"
294
+ onClick={() => clearTabs()}
295
+ title="Close all tabs"
296
+ >
297
+ <MoreHorizontal className="h-3 w-3 mr-1" />
298
+ Close All
299
+ </Button>
300
+ )}
301
+ </div>
302
+ <ScrollBar orientation="horizontal" />
303
+ </ScrollArea>
304
+ </div>
305
+ );
306
+ }
@@ -0,0 +1,214 @@
1
+ "use client";
2
+
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useEffect,
7
+ useRef,
8
+ useState,
9
+ useCallback,
10
+ } from "react";
11
+ import { usePathname } from "next/navigation";
12
+ import { useTabNavigation } from "./tab-navigation-provider";
13
+
14
+ interface RouteCacheContextType {
15
+ reloadTab: (path: string) => void;
16
+ }
17
+
18
+ const RouteCacheContext = createContext<RouteCacheContextType | undefined>(
19
+ undefined,
20
+ );
21
+
22
+ interface CacheEntry {
23
+ node: React.ReactNode;
24
+ lastActive: number;
25
+ path: string;
26
+ }
27
+
28
+ const CACHE_TIMEOUT = 3 * 60 * 1000; // 3 minutes
29
+
30
+ export function RouteCacheProvider({
31
+ children,
32
+ }: {
33
+ children: React.ReactNode;
34
+ }) {
35
+ const pathname = usePathname();
36
+ const { tabs } = useTabNavigation();
37
+ const [cache, setCache] = useState<Map<string, CacheEntry>>(new Map());
38
+ // We need a ref to access the latest cache in effects/callbacks without triggering re-renders
39
+ const cacheRef = useRef<Map<string, CacheEntry>>(new Map());
40
+
41
+ // Update ref when state changes
42
+ useEffect(() => {
43
+ cacheRef.current = cache;
44
+ }, [cache]);
45
+
46
+ // Function to manually reload a tab
47
+ const reloadTab = useCallback((path: string) => {
48
+ setCache((prev) => {
49
+ const newCache = new Map(prev);
50
+ newCache.delete(path);
51
+ return newCache;
52
+ });
53
+ }, []);
54
+
55
+ // 1. Handle Pathname Change (Add/Update Cache)
56
+ useEffect(() => {
57
+ if (!pathname) return;
58
+
59
+ setCache((prev) => {
60
+ const now = Date.now();
61
+ // Check if we need to update
62
+ const existing = prev.get(pathname);
63
+
64
+ // If exists and was active recently (e.g. < 1s), skip update to avoid spam
65
+ // But we need to update lastActive.
66
+ // However, if we just updated it, maybe we don't need to trigger a re-render?
67
+ // Let's just update if it's NOT in cache, or if we want to refresh the timestamp.
68
+
69
+ if (existing) {
70
+ // If it exists, we only update timestamp.
71
+ // To avoid re-renders on every render (if this effect runs often),
72
+ // we could check if timestamp is significantly different?
73
+ // But this effect only runs on [pathname]. So it runs once per navigation.
74
+ // This is fine.
75
+ const newCache = new Map(prev);
76
+ newCache.set(pathname, {
77
+ ...existing,
78
+ lastActive: now,
79
+ });
80
+ return newCache;
81
+ }
82
+
83
+ // If not in cache, add it.
84
+ const newCache = new Map(prev);
85
+ newCache.set(pathname, {
86
+ node: children,
87
+ lastActive: now,
88
+ path: pathname,
89
+ });
90
+ return newCache;
91
+ });
92
+ }, [pathname, children]); // Removed 'tabs' dependency
93
+
94
+ // 2. Handle Tab Closing (Cleanup)
95
+ useEffect(() => {
96
+ // We only want to run this when 'tabs' changes (specifically when a tab is removed)
97
+ // We can check if any cached path is NOT in tabs (and not current path)
98
+
99
+ setCache((prev) => {
100
+ const tabPaths = new Set(tabs.map((t) => t.path));
101
+ let hasChanges = false;
102
+
103
+ // Check if we need to remove anything
104
+ for (const key of prev.keys()) {
105
+ if (key !== pathname && !tabPaths.has(key)) {
106
+ hasChanges = true;
107
+ break;
108
+ }
109
+ }
110
+
111
+ if (!hasChanges) return prev; // Return same reference to avoid re-render
112
+
113
+ const newCache = new Map(prev);
114
+ for (const key of newCache.keys()) {
115
+ if (key !== pathname && !tabPaths.has(key)) {
116
+ newCache.delete(key);
117
+ }
118
+ }
119
+ return newCache;
120
+ });
121
+ }, [tabs, pathname]);
122
+
123
+ // 3. Periodic cleanup for timeouts
124
+ useEffect(() => {
125
+ const interval = setInterval(() => {
126
+ setCache((prev) => {
127
+ const now = Date.now();
128
+ let hasChanges = false;
129
+
130
+ for (const [key, entry] of prev.entries()) {
131
+ if (key === pathname) continue;
132
+ if (now - entry.lastActive > CACHE_TIMEOUT) {
133
+ hasChanges = true;
134
+ break;
135
+ }
136
+ }
137
+
138
+ if (!hasChanges) return prev;
139
+
140
+ const newCache = new Map(prev);
141
+ for (const [key, entry] of newCache.entries()) {
142
+ if (key === pathname) continue;
143
+ if (now - entry.lastActive > CACHE_TIMEOUT) {
144
+ newCache.delete(key);
145
+ }
146
+ }
147
+ return newCache;
148
+ });
149
+ }, 10000);
150
+
151
+ return () => clearInterval(interval);
152
+ }, [pathname]);
153
+
154
+ // Render Logic
155
+ // We render ALL cached items.
156
+ // The one matching `pathname` gets the *fresh* `children` if it wasn't cached,
157
+ // OR the *cached* node if it was.
158
+ // Actually, the logic in the effect above sets the cache.
159
+ // Here we just iterate and render.
160
+
161
+ // WAIT: The `children` passed to this component is the content of the CURRENT route.
162
+ // If we want to cache it, we must save it.
163
+ // But `children` changes on every route change.
164
+
165
+ // Correct Approach for Next.js App Router "Keep Alive":
166
+ // We need to "freeze" the children at the moment they are first mounted for a path.
167
+
168
+ // Let's refine the render:
169
+ // We iterate through the `cache`.
170
+ // For each entry, we render `entry.node`.
171
+ // We control visibility with `display: none`.
172
+
173
+ // BUT: What if the current page is NOT in the cache yet (first render)?
174
+ // The effect hasn't run yet.
175
+ // We should render `children` directly for the current path, and THEN cache it.
176
+
177
+ // Let's try a simpler approach for the render loop:
178
+ const itemsToRender = Array.from(cache.entries());
179
+
180
+ // If current path is not in cache, we must render it too (and it will be added to cache by effect)
181
+ const isCurrentInCache = cache.has(pathname);
182
+
183
+ return (
184
+ <RouteCacheContext.Provider value={{ reloadTab }}>
185
+ <div className="flex-1 w-full h-full relative">
186
+ {itemsToRender.map(([path, entry]) => (
187
+ <div
188
+ key={path}
189
+ className="w-full h-full"
190
+ style={{ display: path === pathname ? "block" : "none" }}
191
+ >
192
+ {entry.node}
193
+ </div>
194
+ ))}
195
+
196
+ {!isCurrentInCache && (
197
+ <div key={pathname} className="w-full h-full">
198
+ {children}
199
+ </div>
200
+ )}
201
+ </div>
202
+ </RouteCacheContext.Provider>
203
+ );
204
+ }
205
+
206
+ export function useRouteCache() {
207
+ const context = useContext(RouteCacheContext);
208
+ if (!context) {
209
+ // It's optional, so we can return a dummy if not found, or throw.
210
+ // Let's return a dummy to avoid breaking if used outside.
211
+ return { reloadTab: () => {} };
212
+ }
213
+ return context;
214
+ }