@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.
- package/package.json +2 -1
- package/src/assets/erp_wallpaper.png +0 -0
- package/src/assets/goeat_logo.png +0 -0
- package/src/audit/audit-manager.ts +139 -0
- package/src/audit/index.ts +11 -0
- package/src/audit/memory-audit-logger.ts +86 -0
- package/src/audit/types.ts +50 -0
- package/src/auth/auth-service.ts +97 -0
- package/src/auth/index.ts +266 -0
- package/src/code-generation/index.ts +69 -0
- package/src/configs/auth-routes.ts +17 -0
- package/src/configs/crud.ts +136 -0
- package/src/configs/data/navigations.ts +781 -0
- package/src/configs/data/oauth-links.ts +10 -0
- package/src/configs/entities/material-categories.config.ts +125 -0
- package/src/configs/i18n.ts +12 -0
- package/src/configs/index.ts +26 -0
- package/src/configs/status.ts +25 -0
- package/src/configs/themes.ts +100 -0
- package/src/crud/components/crud-bulk-actions.tsx +91 -0
- package/src/crud/components/crud-card-view.tsx +241 -0
- package/src/crud/components/crud-context.tsx +122 -0
- package/src/crud/components/crud-delete-dialog.tsx +145 -0
- package/src/crud/components/crud-dialog.tsx +406 -0
- package/src/crud/components/crud-empty-state.tsx +104 -0
- package/src/crud/components/crud-export-button.tsx +170 -0
- package/src/crud/components/crud-field-renderer.tsx +653 -0
- package/src/crud/components/crud-filter-chips.tsx +102 -0
- package/src/crud/components/crud-filters/checkbox-filter.tsx +97 -0
- package/src/crud/components/crud-filters/datetime-filter.tsx +83 -0
- package/src/crud/components/crud-filters/filter-builder.tsx +66 -0
- package/src/crud/components/crud-filters/index.tsx +76 -0
- package/src/crud/components/crud-filters/radio-filter.tsx +86 -0
- package/src/crud/components/crud-filters/select-filter.tsx +141 -0
- package/src/crud/components/crud-filters/text-filter.tsx +86 -0
- package/src/crud/components/crud-form.tsx +642 -0
- package/src/crud/components/crud-import-dialog.tsx +440 -0
- package/src/crud/components/crud-infinite-scroll.tsx +116 -0
- package/src/crud/components/crud-page.tsx +1017 -0
- package/src/crud/components/crud-provider.tsx +277 -0
- package/src/crud/components/crud-row-actions.tsx +189 -0
- package/src/crud/components/crud-search.tsx +82 -0
- package/src/crud/components/crud-sheet.tsx +336 -0
- package/src/crud/components/crud-table-skeleton.tsx +26 -0
- package/src/crud/components/crud-table-toolbar.tsx +91 -0
- package/src/crud/components/crud-table.tsx +352 -0
- package/src/crud/components/crud-virtual-table.tsx +55 -0
- package/src/crud/components/index.tsx +20 -0
- package/src/crud/crud-filters/checkbox-filter.tsx +87 -0
- package/src/crud/crud-filters/datetime-filter.tsx +82 -0
- package/src/crud/crud-filters/filter-builder.tsx +64 -0
- package/src/crud/crud-filters/index.tsx +78 -0
- package/src/crud/crud-filters/radio-filter.tsx +79 -0
- package/src/crud/crud-filters/select-filter.tsx +148 -0
- package/src/crud/crud-filters/text-filter.tsx +81 -0
- package/src/crud/index.ts +43 -0
- package/src/crud/lib/crud-service.test.ts +334 -0
- package/src/crud/lib/crud-service.ts +358 -0
- package/src/crud/lib/crud-utils.test.ts +354 -0
- package/src/crud/lib/crud-utils.ts +299 -0
- package/src/crud/lib/crud-validator.ts +247 -0
- package/src/crud/lib/data-loader.ts +234 -0
- package/src/crud/lib/field-calculator.ts +241 -0
- package/src/crud/lib/field-formatter.ts +240 -0
- package/src/crud/lib/import-export-service.test.ts +290 -0
- package/src/crud/lib/import-export-service.ts +352 -0
- package/src/crud/lib/import-server-utils.ts +109 -0
- package/src/crud/lib/lazy-loader.ts +241 -0
- package/src/crud/lib/parse-filters.ts +85 -0
- package/src/crud/lib/permissions.ts +52 -0
- package/src/crud/lib/serialize-config.ts +60 -0
- package/src/crud/lib/stream-loader.ts +145 -0
- package/src/crud/lib/translate-config.ts +335 -0
- package/src/crud/lib/types.ts +11 -0
- package/src/crud/pages/entity-crud-page.tsx +144 -0
- package/src/crud/server.ts +8 -0
- package/src/home/constants.tsx +142 -0
- package/src/home/feature-showcase.tsx +171 -0
- package/src/home/home-page.tsx +191 -0
- package/src/home/hooks/index.ts +1 -0
- package/src/home/hooks/useWidgetPreferences.ts +167 -0
- package/src/home/index.ts +33 -0
- package/src/home/quick-access-dialog.tsx +271 -0
- package/src/home/quick-access-menu.tsx +267 -0
- package/src/home/types.ts +140 -0
- package/src/home/welcome-card.tsx +92 -0
- package/src/home/widget-container.tsx +258 -0
- package/src/home/widgets/base-widget.tsx +200 -0
- package/src/home/widgets/customers-widget.tsx +74 -0
- package/src/home/widgets/index.ts +6 -0
- package/src/home/widgets/orders-widget.tsx +87 -0
- package/src/home/widgets/revenue-widget.tsx +71 -0
- package/src/home/widgets/stock-widget.tsx +109 -0
- package/src/hooks/index.tsx +598 -0
- package/src/hooks/use-tenant.test.tsx +30 -0
- package/src/hooks/use-tenant.ts +5 -0
- package/src/index.ts +17 -0
- package/src/infrastructure/__tests__/architecture-verification.spec.ts +103 -0
- package/src/infrastructure/api-service.ts +317 -0
- package/src/infrastructure/cache/cache-manager.ts +107 -0
- package/src/infrastructure/cache/cache.ts +120 -0
- package/src/infrastructure/cache/index.ts +8 -0
- package/src/infrastructure/cache/types.ts +48 -0
- package/src/infrastructure/cron/cron-manager.ts +239 -0
- package/src/infrastructure/cron/index.ts +6 -0
- package/src/infrastructure/cron/types.ts +41 -0
- package/src/infrastructure/event-bus/event-bus.ts +145 -0
- package/src/infrastructure/event-bus/index.ts +2 -0
- package/src/infrastructure/event-bus/types.ts +22 -0
- package/src/infrastructure/index.ts +32 -0
- package/src/infrastructure/lock/decorators.ts +67 -0
- package/src/infrastructure/lock/index.ts +2 -0
- package/src/infrastructure/lock/lock-manager.ts +33 -0
- package/src/infrastructure/logger/index.ts +2 -0
- package/src/infrastructure/logger/logger.ts +96 -0
- package/src/infrastructure/logger/types.ts +25 -0
- package/src/layout/index.tsx +185 -0
- package/src/navigation/index.ts +91 -0
- package/src/notification/index.ts +14 -0
- package/src/notification/notification-service.ts +120 -0
- package/src/notification/storage/in-memory.ts +56 -0
- package/src/notification/storage/index.ts +1 -0
- package/src/notification/types.ts +51 -0
- package/src/organization/branch-service.ts +299 -0
- package/src/organization/branches.config.ts +154 -0
- package/src/organization/index.ts +5 -0
- package/src/plugin/apps-registry.ts +97 -0
- package/src/plugin/index.ts +5 -0
- package/src/plugin/types.ts +41 -0
- package/src/providers/index.tsx +109 -0
- package/src/providers/tenant-provider.tsx +45 -0
- package/src/rbac/components/roles/role-card.tsx +158 -0
- package/src/rbac/components/roles/role-stats-cards.tsx +29 -0
- package/src/rbac/components/roles/role-toolbar.tsx +123 -0
- package/src/rbac/hooks/use-role-operations.ts +159 -0
- package/src/rbac/hooks/use-roles-data.ts +59 -0
- package/src/rbac/index.ts +297 -0
- package/src/rbac/lib/permission-helpers.ts +63 -0
- package/src/rbac/pages/action-list-page.tsx +25 -0
- package/src/rbac/pages/resource-list-page.tsx +25 -0
- package/src/rbac/pages/role-list-page.tsx +378 -0
- package/src/rbac/permission-service.ts +140 -0
- package/src/rbac/permissions.ts +135 -0
- package/src/rbac/resource-service.ts +115 -0
- package/src/rbac/resource-validator.ts +119 -0
- package/src/rbac/role-service.ts +165 -0
- package/src/rbac/server.ts +16 -0
- package/src/rbac/types.ts +38 -0
- package/src/schemas/action.schema.ts +66 -0
- package/src/schemas/branch.schema.ts +52 -0
- package/src/schemas/coming-soon-schema.ts +9 -0
- package/src/schemas/company.schema.ts +44 -0
- package/src/schemas/forgot-passward-schema.ts +9 -0
- package/src/schemas/index.ts +30 -0
- package/src/schemas/material-category.schema.ts +43 -0
- package/src/schemas/material-pricing.schema.ts +74 -0
- package/src/schemas/material.schema.ts +76 -0
- package/src/schemas/materials.ts +52 -0
- package/src/schemas/new-passward-schema.ts +15 -0
- package/src/schemas/partner-company.schema.ts +149 -0
- package/src/schemas/register-schema.ts +36 -0
- package/src/schemas/resource.schema.ts +133 -0
- package/src/schemas/role.schema.ts +11 -0
- package/src/schemas/sign-in-schema.ts +24 -0
- package/src/schemas/supplier-pricing.schema.ts +15 -0
- package/src/schemas/supplier.schema.ts +120 -0
- package/src/schemas/system-category-group.schema.ts +67 -0
- package/src/schemas/system-category.schema.ts +77 -0
- package/src/schemas/system-config.schema.ts +118 -0
- package/src/schemas/uom.schema.ts +75 -0
- package/src/schemas/user-supplier.schema.ts +179 -0
- package/src/schemas/user.schema.ts +18 -0
- package/src/schemas/verify-email-schema.ts +9 -0
- package/src/schemas/warehouse.schema.ts +49 -0
- package/src/system/components/categories/category-list.tsx +529 -0
- package/src/system/components/categories/category-manager.tsx +89 -0
- package/src/system/components/categories/group-sidebar.tsx +308 -0
- package/src/system/components/settings/setting-dialogs.tsx +197 -0
- package/src/system/components/settings/setting-field.tsx +291 -0
- package/src/system/components/settings/setting-form-dialog.tsx +308 -0
- package/src/system/components/settings/settings-groups.ts +80 -0
- package/src/system/components/settings/settings-search.tsx +71 -0
- package/src/system/components/settings/settings-section.tsx +74 -0
- package/src/system/components/settings/settings-sidebar.tsx +81 -0
- package/src/system/constants.ts +3 -0
- package/src/system/index.ts +150 -0
- package/src/system/job-manager.ts +176 -0
- package/src/system/pages/components/categories/category-list.tsx +537 -0
- package/src/system/pages/components/categories/category-manager.tsx +90 -0
- package/src/system/pages/components/categories/group-sidebar.tsx +311 -0
- package/src/system/pages/components/settings/sales-rules-settings.tsx +222 -0
- package/src/system/pages/components/settings/setting-dialogs.tsx +197 -0
- package/src/system/pages/components/settings/setting-field.tsx +292 -0
- package/src/system/pages/components/settings/setting-form-dialog.tsx +308 -0
- package/src/system/pages/components/settings/settings-groups.ts +87 -0
- package/src/system/pages/components/settings/settings-page.tsx +372 -0
- package/src/system/pages/components/settings/settings-search.tsx +71 -0
- package/src/system/pages/components/settings/settings-section.tsx +74 -0
- package/src/system/pages/components/settings/settings-sidebar.tsx +81 -0
- package/src/system/pages/components/settings/system-settings.tsx +244 -0
- package/src/system/pages/system-category-page.tsx +15 -0
- package/src/system/pages/system-settings-page.tsx +380 -0
- package/src/system/schemas/system-category-group.schema.ts +46 -0
- package/src/system/schemas/system-category.schema.ts +56 -0
- package/src/system/services/settings-service.ts +127 -0
- package/src/system/services/system-category-service.ts +63 -0
- package/src/system/types.ts +45 -0
- package/src/types/index.ts +703 -0
- package/src/ui/auth/auth-layout.tsx +135 -0
- package/src/ui/auth/forgot-password-form.tsx +98 -0
- package/src/ui/auth/index.tsx +7 -0
- package/src/ui/auth/new-password-form.tsx +107 -0
- package/src/ui/auth/oauth-links.tsx +30 -0
- package/src/ui/auth/register-form.tsx +202 -0
- package/src/ui/auth/sign-in-form.tsx +238 -0
- package/src/ui/auth/verify-email-form.tsx +104 -0
- package/src/ui/crud/index.tsx +10 -0
- package/src/ui/data-display/accordion.tsx +65 -0
- package/src/ui/data-display/aspect-ratio.tsx +11 -0
- package/src/ui/data-display/avatar.tsx +163 -0
- package/src/ui/data-display/bento-grid.tsx +77 -0
- package/src/ui/data-display/carousel.tsx +249 -0
- package/src/ui/data-display/chart.tsx +363 -0
- package/src/ui/data-display/code-block-highlight.tsx +54 -0
- package/src/ui/data-display/collapsible.tsx +42 -0
- package/src/ui/data-display/compact-stat-bar.tsx +149 -0
- package/src/ui/data-display/data-table/data-table-context.tsx +255 -0
- package/src/ui/data-display/data-table/data-table-empty-state.tsx +133 -0
- package/src/ui/data-display/data-table/data-table-skeleton.tsx +145 -0
- package/src/ui/data-display/data-table/data-table-toolbar.tsx +353 -0
- package/src/ui/data-display/data-table/data-table.tsx +597 -0
- package/src/ui/data-display/data-table/index.ts +44 -0
- package/src/ui/data-display/data-table-column-header.tsx +75 -0
- package/src/ui/data-display/data-table-pagination.tsx +130 -0
- package/src/ui/data-display/data-table-view-options.tsx +59 -0
- package/src/ui/data-display/formatted-number-input.tsx +210 -0
- package/src/ui/data-display/highlight.tsx +20 -0
- package/src/ui/data-display/hover-card.tsx +48 -0
- package/src/ui/data-display/index.tsx +50 -0
- package/src/ui/data-display/iphone-15-pro.tsx +114 -0
- package/src/ui/data-display/kanban/index.ts +4 -0
- package/src/ui/data-display/kanban/kanban-board.tsx +192 -0
- package/src/ui/data-display/kanban/kanban-column.tsx +74 -0
- package/src/ui/data-display/kanban/kanban-item.tsx +50 -0
- package/src/ui/data-display/kanban/kanban-types.ts +21 -0
- package/src/ui/data-display/kpi-card.tsx +68 -0
- package/src/ui/data-display/media-grid.tsx +110 -0
- package/src/ui/data-display/safari.tsx +175 -0
- package/src/ui/data-display/show-more-text.tsx +55 -0
- package/src/ui/data-display/tabs.tsx +68 -0
- package/src/ui/data-display/timeline.tsx +256 -0
- package/src/ui/feedback/alert.tsx +60 -0
- package/src/ui/feedback/context-menu.tsx +245 -0
- package/src/ui/feedback/drawer.tsx +132 -0
- package/src/ui/feedback/error-dialog.tsx +273 -0
- package/src/ui/feedback/index.tsx +183 -0
- package/src/ui/feedback/progress.tsx +32 -0
- package/src/ui/feedback/sheet.tsx +148 -0
- package/src/ui/feedback/sonner.tsx +36 -0
- package/src/ui/forms/command.tsx +157 -0
- package/src/ui/forms/date-picker.tsx +73 -0
- package/src/ui/forms/date-range-picker.tsx +76 -0
- package/src/ui/forms/date-time-picker.tsx +109 -0
- package/src/ui/forms/editor/editor-menu-bar.tsx +394 -0
- package/src/ui/forms/editor/index.tsx +130 -0
- package/src/ui/forms/editor/multi-select-example.tsx +1234 -0
- package/src/ui/forms/emoji-picker.tsx +109 -0
- package/src/ui/forms/file-dropzone.tsx +169 -0
- package/src/ui/forms/file-thumbnail.tsx +29 -0
- package/src/ui/forms/index.tsx +201 -0
- package/src/ui/forms/input-file.tsx +99 -0
- package/src/ui/forms/input-group.tsx +46 -0
- package/src/ui/forms/input-otp.tsx +81 -0
- package/src/ui/forms/input-phone.tsx +172 -0
- package/src/ui/forms/input-spin.tsx +116 -0
- package/src/ui/forms/input-tags.tsx +219 -0
- package/src/ui/forms/input-time.tsx +42 -0
- package/src/ui/forms/multi-select.tsx +629 -0
- package/src/ui/forms/multiple-date-picker.tsx +74 -0
- package/src/ui/forms/radio-group.tsx +42 -0
- package/src/ui/forms/rating.tsx +158 -0
- package/src/ui/forms/time-picker.tsx +57 -0
- package/src/ui/index.tsx +17 -0
- package/src/ui/layout/animated-list.tsx +77 -0
- package/src/ui/layout/animated-sidebar.tsx +294 -0
- package/src/ui/layout/command-menu.tsx +355 -0
- package/src/ui/layout/customizer.tsx +324 -0
- package/src/ui/layout/footer.tsx +43 -0
- package/src/ui/layout/full-screen-toggle.tsx +52 -0
- package/src/ui/layout/header-breadcrumb.tsx +77 -0
- package/src/ui/layout/horizontal-layout-header.tsx +83 -0
- package/src/ui/layout/horizontal-layout.tsx +50 -0
- package/src/ui/layout/index.tsx +25 -0
- package/src/ui/layout/language-dropdown.tsx +103 -0
- package/src/ui/layout/logo.tsx +63 -0
- package/src/ui/layout/main-layout.tsx +57 -0
- package/src/ui/layout/mode-dropdown.tsx +58 -0
- package/src/ui/layout/notification-dropdown.tsx +127 -0
- package/src/ui/layout/page-tabs.tsx +306 -0
- package/src/ui/layout/route-cache.tsx +214 -0
- package/src/ui/layout/sidebar-group-icon-menu.tsx +195 -0
- package/src/ui/layout/sidebar.tsx +279 -0
- package/src/ui/layout/tab-content-cache.tsx +201 -0
- package/src/ui/layout/tab-navigation-provider.tsx +536 -0
- package/src/ui/layout/toggle-mobile-sidebar.tsx +33 -0
- package/src/ui/layout/top-bar-header-menubar.tsx +412 -0
- package/src/ui/layout/user-dropdown.tsx +188 -0
- package/src/ui/layout/vertical-layout-header.tsx +65 -0
- package/src/ui/layout/vertical-layout.tsx +47 -0
- package/src/ui/management/audit-log-page.tsx +209 -0
- package/src/ui/management/cache-management.tsx +349 -0
- package/src/ui/management/index.ts +3 -0
- package/src/ui/management/job-management.tsx +308 -0
- package/src/ui/pages/not-found.tsx +30 -0
- package/src/ui/primitives/badge.tsx +66 -0
- package/src/ui/primitives/breadcrumb.tsx +103 -0
- package/src/ui/primitives/button.tsx +129 -0
- package/src/ui/primitives/calendar.tsx +74 -0
- package/src/ui/primitives/card.tsx +86 -0
- package/src/ui/primitives/checkbox.tsx +31 -0
- package/src/ui/primitives/client.ts +30 -0
- package/src/ui/primitives/combobox.tsx +290 -0
- package/src/ui/primitives/dialog.tsx +121 -0
- package/src/ui/primitives/dropdown-menu.tsx +239 -0
- package/src/ui/primitives/dynamic-icon.tsx +24 -0
- package/src/ui/primitives/index.tsx +134 -0
- package/src/ui/primitives/input-number.tsx +131 -0
- package/src/ui/primitives/input.tsx +22 -0
- package/src/ui/primitives/keyboard.tsx +23 -0
- package/src/ui/primitives/label.tsx +24 -0
- package/src/ui/primitives/menubar.tsx +262 -0
- package/src/ui/primitives/navigation-menu.tsx +157 -0
- package/src/ui/primitives/pagination.tsx +118 -0
- package/src/ui/primitives/popover.tsx +56 -0
- package/src/ui/primitives/prefetch-link.tsx +60 -0
- package/src/ui/primitives/resizable.tsx +59 -0
- package/src/ui/primitives/scroll-area.tsx +63 -0
- package/src/ui/primitives/select.tsx +172 -0
- package/src/ui/primitives/separator.tsx +51 -0
- package/src/ui/primitives/sidebar.tsx +844 -0
- package/src/ui/primitives/slider.tsx +27 -0
- package/src/ui/primitives/status-badge.tsx +47 -0
- package/src/ui/primitives/sticky-layout.tsx +50 -0
- package/src/ui/primitives/switch.tsx +29 -0
- package/src/ui/primitives/table.tsx +116 -0
- package/src/ui/primitives/tabs.tsx +55 -0
- package/src/ui/primitives/toggle-group.tsx +70 -0
- package/src/ui/primitives/toggle.tsx +47 -0
- package/src/ui/primitives/tooltip.tsx +59 -0
- package/src/user/components/dangerous-zone.tsx +34 -0
- package/src/user/components/delete-account-form.tsx +40 -0
- package/src/user/components/index.ts +4 -0
- package/src/user/components/profile-info-form.tsx +390 -0
- package/src/user/components/profile-info.tsx +32 -0
- package/src/user/components/unified-profile-dialog.tsx +1019 -0
- package/src/user/components/user-stats.tsx +27 -0
- package/src/user/components/user-toolbar.tsx +137 -0
- package/src/user/components/users-card-view.tsx +253 -0
- package/src/user/index.ts +11 -0
- package/src/user/pages/user-list-page.tsx +234 -0
- package/src/user/pages/users-client-page.tsx +385 -0
- package/src/user/profile-page.tsx +19 -0
- package/src/user/schemas.ts +68 -0
- package/src/user/types.ts +34 -0
- package/src/user/user-service.ts +538 -0
- package/src/utils/index.ts +906 -0
- package/src/workflow/activity-timeline.tsx +412 -0
- package/src/workflow/approval-workflow.tsx +31 -0
- 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
|
+
}
|