From 1a73a8f6c843732566d69079b1a1ec866eb7476f Mon Sep 17 00:00:00 2001 From: MohamedHasan07 Date: Tue, 14 Jul 2026 12:45:22 +0530 Subject: [PATCH 1/2] notifiction,auditlog,themes --- docs/adr/ADR-001-notification-architecture.md | 164 ++++ generate-messages.cjs | 109 +++ package-lock.json | 86 +++ package.json | 1 + public/images/404.svg | 3 + src/App.tsx | 34 +- src/api/axiosInstance.ts | 8 +- src/assets/error_page_images/404.svg | 3 + src/components/customs/Loader.tsx | 174 ++++- src/components/customs/PageLoader.tsx | 125 +++ src/components/customs/Toast.css | 0 src/components/layouts/Headers.tsx | 43 +- src/components/layouts/MainLayout.tsx | 9 +- src/constants/themes.ts | 111 +++ .../asset-families/hook/useAssetFamily.ts | 24 +- src/features/asset-types/hook/useAssetType.ts | 24 +- src/features/assets/hook/useAsset.ts | 24 +- .../hook/useAttributeGroup.ts | 24 +- src/features/attributes/hook/useAttribute.ts | 27 +- src/features/brands/hook/useBrand.ts | 26 +- src/features/categories/hook/useCategory.ts | 20 +- .../channel-types/hook/useChannelType.ts | 24 +- src/features/channels/hook/useChannel.ts | 24 +- src/features/channels/pages/ChannelList.tsx | 14 +- src/features/channels/pages/NewChannel.tsx | 38 +- src/features/family/hook/useFamily.ts | 27 +- .../integrations/hook/useIntegration.ts | 24 +- .../components/NotificationCard.tsx | 145 ++++ .../components/NotificationEmptyState.tsx | 25 + .../components/NotificationFilterBar.tsx | 65 ++ .../components/NotificationPageHeader.tsx | 51 ++ .../components/NotificationSearchBar.tsx | 38 + .../components/NotificationSkeleton.tsx | 35 + .../components/NotificationTimeline.tsx | 86 +++ .../constants/notification.constants.ts | 44 ++ .../notifications/data/notifications.mock.ts | 211 ++++++ .../notifications/hooks/useNotifications.ts | 181 +++++ src/features/notifications/index.ts | 10 + .../notifications/pages/NotificationsPage.tsx | 70 ++ .../routes/notifications.routes.tsx | 12 + .../services/notification.service.ts | 103 +++ .../notifications/types/notification.types.ts | 69 ++ src/features/product/hook/useProduct.ts | 24 +- src/features/reports/hook/useReport.ts | 24 +- src/features/roles/hooks/useRole.ts | 42 +- src/features/roles/pages/EditRoleForm.tsx | 296 -------- src/features/roles/pages/NewRoleForm.tsx | 716 ++++++++++++------ src/features/roles/routes/role.routes.tsx | 3 +- src/features/roles/services/role.service.ts | 52 +- src/features/roles/types/role.types.ts | 68 +- src/features/settings/hook/useSetting.ts | 24 +- src/features/settings/pages/ThemeSettings.tsx | 5 +- src/features/tenants/hooks/useTenant.ts | 26 +- src/features/units/hook/useUnit.ts | 24 +- src/features/users/hook/useUser.ts | 24 +- src/features/variants/hook/useVariant.ts | 24 +- src/features/workflow/hook/useWorkflow.ts | 24 +- src/index.css | 33 +- src/routes/AppRoutes.tsx | 4 + src/routes/routeConfig.ts | 3 + src/routes/sidebar.config.ts | 8 +- src/services/socket.service.ts | 74 ++ src/services/theme.service.ts | 120 +-- src/services/toast/components/CloseButton.tsx | 23 + src/services/toast/components/ProgressBar.tsx | 38 + src/services/toast/components/Toast.css | 300 ++++++++ src/services/toast/components/Toast.tsx | 148 ++++ .../toast/components/ToastContainer.tsx | 8 + src/services/toast/components/ToastIcons.tsx | 12 + src/services/toast/hooks/useToast.ts | 10 + .../toast/hooks/useToastRouteCleanup.ts | 33 + src/services/toast/index.ts | 29 + src/services/toast/messages/index.ts | 7 + .../toast/messages/system.messages.ts | 45 ++ src/services/toast/toast.config.ts | 73 ++ src/services/toast/toast.errors.ts | 103 +++ src/services/toast/toast.factory.ts | 55 ++ src/services/toast/toast.icons.tsx | 83 ++ src/services/toast/toast.internal.ts | 59 ++ src/services/toast/toast.promise.ts | 78 ++ src/services/toast/toast.provider.tsx | 36 + src/services/toast/toast.registry.ts | 76 ++ src/services/toast/toast.service.ts | 195 +++++ src/services/toast/toast.types.ts | 93 +++ src/services/toast/toast.utils.ts | 149 ++++ 85 files changed, 4613 insertions(+), 993 deletions(-) create mode 100644 docs/adr/ADR-001-notification-architecture.md create mode 100644 generate-messages.cjs create mode 100644 public/images/404.svg create mode 100644 src/assets/error_page_images/404.svg create mode 100644 src/components/customs/PageLoader.tsx delete mode 100644 src/components/customs/Toast.css create mode 100644 src/constants/themes.ts create mode 100644 src/features/notifications/components/NotificationCard.tsx create mode 100644 src/features/notifications/components/NotificationEmptyState.tsx create mode 100644 src/features/notifications/components/NotificationFilterBar.tsx create mode 100644 src/features/notifications/components/NotificationPageHeader.tsx create mode 100644 src/features/notifications/components/NotificationSearchBar.tsx create mode 100644 src/features/notifications/components/NotificationSkeleton.tsx create mode 100644 src/features/notifications/components/NotificationTimeline.tsx create mode 100644 src/features/notifications/constants/notification.constants.ts create mode 100644 src/features/notifications/data/notifications.mock.ts create mode 100644 src/features/notifications/hooks/useNotifications.ts create mode 100644 src/features/notifications/index.ts create mode 100644 src/features/notifications/pages/NotificationsPage.tsx create mode 100644 src/features/notifications/routes/notifications.routes.tsx create mode 100644 src/features/notifications/services/notification.service.ts create mode 100644 src/features/notifications/types/notification.types.ts delete mode 100644 src/features/roles/pages/EditRoleForm.tsx create mode 100644 src/services/socket.service.ts create mode 100644 src/services/toast/components/CloseButton.tsx create mode 100644 src/services/toast/components/ProgressBar.tsx create mode 100644 src/services/toast/components/Toast.css create mode 100644 src/services/toast/components/Toast.tsx create mode 100644 src/services/toast/components/ToastContainer.tsx create mode 100644 src/services/toast/components/ToastIcons.tsx create mode 100644 src/services/toast/hooks/useToast.ts create mode 100644 src/services/toast/hooks/useToastRouteCleanup.ts create mode 100644 src/services/toast/index.ts create mode 100644 src/services/toast/messages/index.ts create mode 100644 src/services/toast/messages/system.messages.ts create mode 100644 src/services/toast/toast.config.ts create mode 100644 src/services/toast/toast.errors.ts create mode 100644 src/services/toast/toast.factory.ts create mode 100644 src/services/toast/toast.icons.tsx create mode 100644 src/services/toast/toast.internal.ts create mode 100644 src/services/toast/toast.promise.ts create mode 100644 src/services/toast/toast.provider.tsx create mode 100644 src/services/toast/toast.registry.ts create mode 100644 src/services/toast/toast.service.ts create mode 100644 src/services/toast/toast.types.ts create mode 100644 src/services/toast/toast.utils.ts diff --git a/docs/adr/ADR-001-notification-architecture.md b/docs/adr/ADR-001-notification-architecture.md new file mode 100644 index 0000000..207fed2 --- /dev/null +++ b/docs/adr/ADR-001-notification-architecture.md @@ -0,0 +1,164 @@ +# ADR-001: Notification Architecture + +## Status + +Accepted + +## Date + +2025 + +## Context + +The application required a notification system capable of displaying toast messages across all feature modules. The initial implementation used scattered legacy helpers (`showSuccess`, `showError`, `showWarning`, `showInfo`) imported directly from internal toast service files. This created tight coupling between feature modules and the notification infrastructure, made future extension difficult, and produced inconsistent error handling across the codebase. + +A migration was undertaken to replace this with a domain-agnostic, extensible notification infrastructure. + +## Decision + +The notification system is implemented as a domain-agnostic infrastructure service with a single stable public API. + +### Ownership boundaries + +**Feature modules own:** +- Business messages (title, description, variant) +- The decision of when to notify + +**Notification infrastructure owns:** +- Rendering (Toast components, icons, progress bar, animations) +- Lifecycle management (auto-close, dismiss, update) +- Promise handling (loading → success/error transitions) +- Error normalization (Axios, Fetch, AbortError, unknown) +- Accessibility (aria-live, aria-label, role) +- Event dispatching (registry) +- Queue management (max visible toasts, suppression rules) + +### Constraints + +- The notification infrastructure must not contain business-specific concepts such as Product, Category, Asset, Workflow, Role, or any domain entity. +- All notifications enter the system through the `notify` facade — no feature module may import from internal toast files. +- The public API is frozen. New methods must not be added without a deliberate cross-cutting justification. + +### Public API (frozen at v1.0.0) + +```ts +notify.success(content, options?, metadata?) +notify.error(content, options?, metadata?) +notify.warning(content, options?, metadata?) +notify.info(content, options?, metadata?) +notify.loading(content, options?) +notify.promise(promise, config) +notify.update(id, variant, content, options?) +notify.dismiss(id) +notify.dismissAll() +``` + +### Allowed imports in feature modules + +```ts +import { notify } from '@/services/toast'; +// or +const { notify } = useToast(); +``` + +Nothing else from the toast infrastructure is part of the public contract. + +## Event flow + +``` +Feature Module + │ + ▼ +notify.success(...) / notify.error(...) / notify.promise(...) + │ + ▼ +Notification Service (toast.service.ts) + │ + ▼ +Notification Registry (toast.registry.ts) + │ + ┌────┴────────────────────┐ + ▼ ▼ +Toast Renderer Future Subscribers +(react-toastify) │ + ┌────────┼──────────────┐ + ▼ ▼ ▼ + Header Bell Notification Analytics / + Store Telemetry +``` + +## Rationale + +This design decouples the notification contract from its consumers. The `notify` facade is the only surface feature modules depend on. Everything behind it — the toast renderer today, a notification store tomorrow, analytics later — can be added, replaced, or extended without touching a single feature module. + +The registry (`toast.registry.ts`) is the mechanism that makes this possible. It is an internal event dispatcher. Any infrastructure component can subscribe to it: + +```ts +// Future: notification store (infrastructure only) +import { subscribe } from '@/services/toast/toast.registry'; + +subscribe((notification) => notificationStore.add(notification)); +``` + +```ts +// Future: header bell (infrastructure only) +import { subscribe } from '@/services/toast/toast.registry'; + +subscribe((notification) => { + if (notification.priority === 'high' || notification.priority === 'critical') { + bellStore.increment(); + } +}); +``` + +Feature modules are never aware these consumers exist. + +Every notification already carries `timestamp`, `module`, `entity`, `entityId`, `action`, `correlationId`, and `userId` through `ToastMetadata` — all the fields a Notification History panel needs without any future API changes. + +## Consequences + +### Positive +- All 22 feature modules use a single, consistent notification API. +- Error normalization is centralized — Axios errors, network failures, and unknown errors are handled uniformly without feature modules needing to know the error shape. +- Future consumers (Notification Center, Header Bell, Analytics, WebSocket queue) can be added with zero changes to feature code. +- The public API is small, stable, and easy to audit. + +### Constraints going forward +- Do not add business-specific helpers (`notify.product.created()`). +- Do not add domain message catalogs to the toast infrastructure layer. +- Do not expand the public API unless there is a compelling cross-cutting need that cannot be handled in a feature module. +- New notification consumers must subscribe through the registry — they must not intercept or wrap the `notify` facade. + +## Infrastructure file map + +``` +src/services/toast/ +├── index.ts ← public API surface (frozen) +├── toast.service.ts ← notify facade implementation +├── toast.provider.tsx ← ToastContainer initialization +├── toast.registry.ts ← internal event dispatcher +├── toast.internal.ts ← low-level ops (loading, update) +├── toast.promise.ts ← promise lifecycle +├── toast.errors.ts ← error normalization +├── toast.factory.ts ← message builders +├── toast.config.ts ← durations, positions, suppression +├── toast.types.ts ← all shared types +├── toast.utils.ts ← variant tokens, payload helpers +├── toast.icons.tsx ← animated SVG icons +├── components/ +│ ├── Toast.tsx ← ToastBody + variant exports +│ ├── Toast.css ← all animations +│ ├── ProgressBar.tsx ← auto-close progress bar +│ ├── CloseButton.tsx ← dismiss button +│ ├── ToastContainer.tsx ← re-export of react-toastify container +│ └── ToastIcons.tsx ← re-export of toast.icons +├── hooks/ +│ └── useToast.ts ← React hook returning notify +└── messages/ + ├── index.ts ← exports SystemMessages + └── system.messages.ts ← infrastructure-level error messages only +``` + +## Version + +`v1.0.0 — feat(toast): complete enterprise notification system migration` diff --git a/generate-messages.cjs b/generate-messages.cjs new file mode 100644 index 0000000..79640ec --- /dev/null +++ b/generate-messages.cjs @@ -0,0 +1,109 @@ +const fs = require('fs'); +const path = require('path'); + +const dir = 'c:/Users/Lenovo/Desktop/product_catalogue/productcatalogue_frontend/src/services/toast/messages'; +if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + +function toPascal(str) { + return str.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(''); +} + +function toTitle(str) { + return str.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '); +} + +const entities = [ + 'product', 'category', 'family', 'variant', 'attribute', 'attribute-group', + 'brand', 'unit', 'channel', 'channel-type', 'asset', 'asset-family', + 'workflow', 'role', 'permission', 'tenant', 'integration', 'report', + 'settings', 'theme', 'auth', 'user', 'system' +]; + +entities.forEach(entity => { + const isSystem = ['system', 'auth', 'settings', 'theme', 'workflow'].includes(entity); + const PascalName = toPascal(entity); + const TitleName = toTitle(entity); + + let content = `import { createSuccess, createError, createWarning, createInfo } from '../toast.factory';\n\n`; + content += `export const ${PascalName}Messages = {\n`; + + if (entity === 'system') { + content += ` networkError: () => createError("Network Error", "Unable to connect to the server. Please check your internet connection."), + validationError: (msg?: string) => createError("Validation Failed", msg || "Please check the form for errors."), + duplicateRecord: () => createError("Duplicate Record", "A record with this information already exists."), + permissionDenied: () => createError("Permission Denied", "You do not have permission to perform this action."), + unauthorized: () => createError("Unauthorized", "Please log in to continue."), + conflict: () => createError("Conflict", "The resource has been modified by another user."), + notFound: () => createError("Not Found", "The requested resource could not be found."), + timeout: () => createError("Timeout", "The request took too long to complete."), + internalServerError: () => createError("Server Error", "An unexpected error occurred. Please try again later."), + unknownError: (msg?: string) => createError("Unknown Error", msg || "An unexpected error occurred."),\n`; + } else if (entity === 'auth') { + content += ` login: () => createSuccess("Logged In", "You have been logged in successfully."), + logout: () => createSuccess("Logged Out", "You have been logged out successfully."), + sessionExpired: () => createWarning("Session Expired", "Your session has expired. Please log in again."),\n`; + } else if (entity === 'settings') { + content += ` saved: () => createSuccess("Settings Saved", "Application settings have been saved successfully."), + reset: () => createSuccess("Settings Reset", "Application settings have been restored to defaults."),\n`; + } else if (entity === 'theme') { + content += ` applied: () => createSuccess("Theme Applied", "The selected theme has been applied."), + reset: () => createSuccess("Theme Reset", "The theme has been restored to default."),\n`; + } else if (entity === 'workflow') { + content += ` submitted: () => createSuccess("Workflow Submitted", "The record has been submitted for review."), + approved: () => createSuccess("Workflow Approved", "The record has been approved."), + rejected: () => createError("Workflow Rejected", "The submission has been returned for revision."), + returned: () => createWarning("Workflow Returned", "The workflow has been returned."), + published: () => createSuccess("Workflow Published", "The workflow has been published."), + unpublished: () => createSuccess("Workflow Unpublished", "The workflow has been unpublished."),\n`; + } else if (entity === 'asset') { + content += ` uploaded: () => createSuccess("Asset Uploaded", "The asset has been uploaded successfully."), + replaced: () => createSuccess("Asset Replaced", "The asset has been replaced successfully."), + linked: () => createSuccess("Asset Linked", "The asset has been linked successfully."), + unlinked: () => createSuccess("Asset Unlinked", "The asset has been unlinked successfully."), + archived: () => createSuccess("Asset Archived", "The asset has been archived."), + deleted: () => createSuccess("Asset Deleted", "The asset has been removed from the system."),\n`; + } else if (entity === 'role') { + content += ` created: () => createSuccess("Role Created", "The role has been added successfully."), + updated: () => createSuccess("Role Updated", "The role information has been updated successfully."), + permissionsChanged: () => createSuccess("Permissions Updated", "Role permissions have been updated."), + deleted: () => createSuccess("Role Deleted", "The role has been removed from the system."),\n`; + } else if (entity === 'user') { + content += ` created: () => createSuccess("User Created", "The user has been added to the system."), + updated: () => createSuccess("User Updated", "The user information has been updated successfully."), + disabled: () => createWarning("User Disabled", "The user has been disabled."), + enabled: () => createSuccess("User Enabled", "The user has been enabled."), + passwordReset: () => createSuccess("Password Reset", "The user's password has been reset."), + deleted: () => createSuccess("User Deleted", "The user has been removed from the system."),\n`; + } else if (entity === 'category') { + content += ` created: () => createSuccess("Category Created", "The category has been added to the catalogue."), + updated: () => createSuccess("Category Updated", "The category information has been updated successfully."), + deleted: () => createSuccess("Category Deleted", "The category has been removed from the system."), + moved: () => createSuccess("Category Moved", "The category has been moved successfully."), + merged: () => createSuccess("Category Merged", "The categories have been merged successfully."), + split: () => createSuccess("Category Split", "The category has been split successfully."),\n`; + } else { + // Standard CRUD + content += ` created: () => createSuccess("${TitleName} Created", "The ${TitleName.toLowerCase()} has been added to the catalogue."), + updated: () => createSuccess("${TitleName} Updated", "The ${TitleName.toLowerCase()} information has been updated successfully."), + deleted: () => createSuccess("${TitleName} Deleted", "The ${TitleName.toLowerCase()} has been removed from the system."), + archived: () => createSuccess("${TitleName} Archived", "The ${TitleName.toLowerCase()} has been archived."), + restored: () => createSuccess("${TitleName} Restored", "The ${TitleName.toLowerCase()} has been restored."), + published: () => createSuccess("${TitleName} Published", "The ${TitleName.toLowerCase()} has been published."), + unpublished: () => createSuccess("${TitleName} Unpublished", "The ${TitleName.toLowerCase()} has been unpublished."), + cloned: () => createSuccess("${TitleName} Cloned", "The ${TitleName.toLowerCase()} has been cloned successfully."), + imported: () => createSuccess("${TitleName} Imported", "The ${TitleName.toLowerCase()}s have been imported successfully."), + exported: () => createSuccess("${TitleName} Exported", "The ${TitleName.toLowerCase()}s have been exported successfully."),\n`; + if (entity === 'product') { + content += ` variantGenerated: () => createSuccess("Variant Generated", "The product variants have been generated successfully."),\n`; + } + } + + content += `};\n`; + + fs.writeFileSync(path.join(dir, `${entity}.messages.ts`), content); +}); + +// Create index.ts +let indexContent = entities.map(entity => `export * from './${entity}.messages';`).join('\n'); +fs.writeFileSync(path.join(dir, 'index.ts'), indexContent + '\n'); +console.log('Done generating messages'); diff --git a/package-lock.json b/package-lock.json index d48388b..4f9394a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "react-router-dom": "^7.18.0", "react-toastify": "^11.1.0", "recharts": "^3.8.1", + "socket.io-client": "^4.8.3", "tailwind-merge": "^3.6.0", "tailwindcss": "^4.3.1", "yup": "^1.7.1" @@ -1663,6 +1664,12 @@ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "license": "MIT" }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -2879,6 +2886,28 @@ "dev": true, "license": "ISC" }, + "node_modules/engine.io-client": { + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.6.tgz", + "integrity": "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.21.6", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", @@ -4583,6 +4612,34 @@ "node": ">=8" } }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5294,6 +5351,35 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 60e2525..33cde64 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "react-router-dom": "^7.18.0", "react-toastify": "^11.1.0", "recharts": "^3.8.1", + "socket.io-client": "^4.8.3", "tailwind-merge": "^3.6.0", "tailwindcss": "^4.3.1", "yup": "^1.7.1" diff --git a/public/images/404.svg b/public/images/404.svg new file mode 100644 index 0000000..6bbe284 --- /dev/null +++ b/public/images/404.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 72ad1f2..a9e05b5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,13 +1,13 @@ // src/App.tsx import { useEffect, useRef, useState } from 'react'; import AppRoutes from './routes/AppRoutes'; -import { ToastContainer } from 'react-toastify'; import { LanguageProvider } from './contexts/LanguageContext'; import { HeaderProvider } from './contexts/HeaderContext'; import { ThemeProvider } from './contexts/ThemeContext'; -import { Loader } from "./components/customs/Loader"; +import { Loader } from './components/customs/Loader'; +import { ToastProvider } from './services/toast'; -function App() { +function AppShell() { const bootstrappedRef = useRef(false); const [bootstrapped, setBootstrapped] = useState(false); const [progress, setProgress] = useState(0); @@ -17,14 +17,12 @@ function App() { bootstrappedRef.current = true; const bootstrap = async () => { - const steps = [15, 35, 55, 75, 92, 100]; - + const steps = [12, 28, 48, 65, 82, 95, 100]; for (const step of steps) { - await new Promise(resolve => setTimeout(resolve, 160)); + await new Promise(resolve => setTimeout(resolve, 150)); setProgress(step); } - - await new Promise(resolve => setTimeout(resolve, 300)); + await new Promise(resolve => setTimeout(resolve, 250)); setBootstrapped(true); }; @@ -43,16 +41,22 @@ function App() { ); } + return ( + + + + + + + ); +} + +function App() { return ( - - - - - - + ); } -export default App; \ No newline at end of file +export default App; diff --git a/src/api/axiosInstance.ts b/src/api/axiosInstance.ts index 27a971c..7be0f71 100644 --- a/src/api/axiosInstance.ts +++ b/src/api/axiosInstance.ts @@ -35,7 +35,7 @@ axiosInstance.interceptors.response.use( ); const apiClient = { - get: async (url: string, params?: any): Promise => { + get: async (url: string, params?: Record): Promise => { const response = await axiosInstance.get(url, { params }); return response.data; }, @@ -47,7 +47,11 @@ const apiClient = { const response = await axiosInstance.put(url, data); return response.data; }, - delete: async (url: string, params?: any): Promise => { + patch: async (url: string, data?: unknown): Promise => { + const response = await axiosInstance.patch(url, data); + return response.data; + }, + delete: async (url: string, params?: Record): Promise => { const response = await axiosInstance.delete(url, { params }); return response.data; }, diff --git a/src/assets/error_page_images/404.svg b/src/assets/error_page_images/404.svg new file mode 100644 index 0000000..6bbe284 --- /dev/null +++ b/src/assets/error_page_images/404.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/customs/Loader.tsx b/src/components/customs/Loader.tsx index b60961e..eeb1c4c 100644 --- a/src/components/customs/Loader.tsx +++ b/src/components/customs/Loader.tsx @@ -1,7 +1,39 @@ // src/components/customs/Loader.tsx -import { Package } from "lucide-react"; import { cn } from "../../lib/utils"; +// Generate the 12-peak wavy ring path mathematically for SVG viewBox 0 0 100 100 +const { WAVY_PATH, TOTAL_LENGTH } = (() => { + const points = 360; // 360 points for perfect vector smoothness + const r0 = 38; + const amplitude = 3; // soft curves + const wavesCount = 12; + const pathParts = []; + let length = 0; + let prevX = 0; + let prevY = 0; + + for (let i = 0; i <= points; i++) { + const theta = (i / points) * Math.PI * 2; + const r = r0 + amplitude * Math.cos(wavesCount * theta); + const x = 50 + r * Math.cos(theta); + const y = 50 + r * Math.sin(theta); + + pathParts.push(`${i === 0 ? "M" : "L"} ${x.toFixed(3)} ${y.toFixed(3)}`); + + if (i > 0) { + const dx = x - prevX; + const dy = y - prevY; + length += Math.sqrt(dx * dx + dy * dy); + } + prevX = x; + prevY = y; + } + return { + WAVY_PATH: pathParts.join(" ") + " Z", + TOTAL_LENGTH: length + }; +})(); + export interface LoaderProps { size?: "sm" | "md" | "lg" | "xl"; message?: string; @@ -9,6 +41,7 @@ export interface LoaderProps { progress?: number; fullScreen?: boolean; className?: string; + color?: string; // Optional custom solid color (e.g. #7C3AED) } export function Loader({ @@ -18,51 +51,132 @@ export function Loader({ progress, fullScreen = false, className, + color, }: LoaderProps) { + const iconSize = size === "xl" ? 56 : size === "lg" ? 44 : size === "md" ? 32 : 24; + const outerSize = iconSize * 2.2; + const barWidth = size === "xl" ? 220 : size === "lg" ? 180 : size === "md" ? 140 : 100; + const barHeight = size === "xl" ? 4 : size === "lg" ? 4 : 3; + const strokeColor = color || "var(--color-primary, #7C3AED)"; + return (
-
-
- -
+ {/* Inline styles for local animation */} +