diff --git a/.gitignore b/.gitignore index 17c1bb6..e893f03 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ businessmanagementapp1/ .history .env +.github/copilot-instructions.md +__pycache__/ +*.pyc diff --git a/client/README.md b/client/README.md deleted file mode 100644 index e215bc4..0000000 --- a/client/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/client/jest.config.js b/client/jest.config.js index d3d0ef3..01c1b5a 100644 --- a/client/jest.config.js +++ b/client/jest.config.js @@ -5,6 +5,8 @@ module.exports = { moduleNameMapper: { // Support for @ alias '^@/(.*)$': '/src/$1', + // Mock CSS imports + '\\.(css|less|scss|sass)$': 'identity-obj-proxy', }, globals: { 'ts-jest': { diff --git a/client/next.config.ts b/client/next.config.ts index e9ffa30..4a91afa 100644 --- a/client/next.config.ts +++ b/client/next.config.ts @@ -1,7 +1,9 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + images: { + domains: ["localhost"], + }, }; export default nextConfig; diff --git a/client/package-lock.json b/client/package-lock.json index 76132f9..89ab192 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,23 +8,61 @@ "name": "slotify", "version": "0.1.0", "dependencies": { + "@hookform/resolvers": "^5.2.1", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-aspect-ratio": "^1.1.7", + "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-context-menu": "^2.2.16", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-menubar": "^1.1.16", + "@radix-ui/react-navigation-menu": "^1.2.14", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", + "@reduxjs/toolkit": "^2.9.0", "axios": "^1.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^4.1.0", + "embla-carousel-react": "^8.6.0", + "input-otp": "^1.4.2", "js-cookie": "^3.0.5", "lightningcss": "^1.30.1", "lucide-react": "^0.454.0", "next": "15.2.4", + "next-themes": "^0.4.6", "postcss": "^8.5", "react": "^19", + "react-day-picker": "^9.9.0", "react-dom": "^19", + "react-hook-form": "^7.62.0", + "react-icons": "^5.5.0", + "react-redux": "^9.2.0", + "react-resizable-panels": "^3.0.5", + "recharts": "^2.15.4", + "sonner": "^2.0.7", + "swr": "^2.3.6", "tailwind-merge": "^2.5.5", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "vaul": "^1.1.2", + "zod": "^4.1.5" }, "devDependencies": { "@babel/preset-env": "^7.28.3", @@ -45,6 +83,7 @@ "babel-jest": "^30.1.2", "eslint": "^9", "eslint-config-next": "15.4.7", + "identity-obj-proxy": "^3.0.0", "jest": "^30.1.2", "jest-environment-jsdom": "^30.1.2", "tailwindcss": "^4.1.9", @@ -1994,7 +2033,6 @@ "version": "7.28.3", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2170,6 +2208,12 @@ "node": ">=18" } }, + "node_modules/@date-fns/tz": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz", + "integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==", + "license": "MIT" + }, "node_modules/@emnapi/core": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", @@ -2382,6 +2426,18 @@ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, + "node_modules/@hookform/resolvers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.1.tgz", + "integrity": "sha512-u0+6X58gkjMcxur1wRWokA7XsiiBJ6aK17aPZxhkoYiK5J+HcTx0Vhu9ovXe6H+dVpO6cjrn2FkJTryXEMlryQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -3705,6 +3761,65 @@ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", @@ -3728,6 +3843,56 @@ } } }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", + "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-checkbox": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", @@ -3758,6 +3923,36 @@ } } }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", @@ -3814,6 +4009,70 @@ } } }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", @@ -3856,6 +4115,35 @@ } } }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-focus-guards": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", @@ -3896,6 +4184,37 @@ } } }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-id": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", @@ -3937,22 +4256,455 @@ } } }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", "license": "MIT", "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -3969,38 +4721,37 @@ } } }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", "license": "MIT", "dependencies": { + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -4017,13 +4768,20 @@ } } }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.2.3" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", @@ -4040,33 +4798,15 @@ } } }, - "node_modules/@radix-ui/react-select": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", - "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", "license": "MIT", "dependencies": { - "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", @@ -4083,37 +4823,53 @@ } } }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-switch": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", - "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" + "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -4200,6 +4956,24 @@ } } }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-layout-effect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", @@ -4295,6 +5069,32 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, + "node_modules/@reduxjs/toolkit": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz", + "integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -4336,6 +5136,18 @@ "@sinonjs/commons": "^3.0.1" } }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -4799,6 +5611,69 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -4956,6 +5831,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -6439,6 +7320,22 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -6535,55 +7432,175 @@ "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.25.3" + "browserslist": "^4.25.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" + "engines": { + "node": ">=12" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "d3-path": "^3.1.0" }, "engines": { - "node": ">= 8" + "node": ">=12" } }, - "node_modules/css.escape": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", - "dev": true, - "license": "MIT" + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } }, - "node_modules/cssstyle": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", - "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", - "dev": true, - "license": "MIT", + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", "dependencies": { - "@asamuzakjp/css-color": "^3.2.0", - "rrweb-cssom": "^0.8.0" + "d3-time": "1 - 3" }, "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true, - "license": "MIT" + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -6660,6 +7677,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-jalali": { + "version": "4.1.0-0", + "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz", + "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -6685,6 +7718,12 @@ "dev": true, "license": "MIT" }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/dedent": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", @@ -6766,9 +7805,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -6819,6 +7856,16 @@ "license": "MIT", "peer": true }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -6847,6 +7894,34 @@ "dev": true, "license": "ISC" }, + "node_modules/embla-carousel": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", + "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", + "license": "MIT" + }, + "node_modules/embla-carousel-react": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.6.0.tgz", + "integrity": "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==", + "license": "MIT", + "dependencies": { + "embla-carousel": "8.6.0", + "embla-carousel-reactive-utils": "8.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/embla-carousel-reactive-utils": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz", + "integrity": "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==", + "license": "MIT", + "peerDependencies": { + "embla-carousel": "8.6.0" + } + }, "node_modules/emittery": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", @@ -7547,6 +8622,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -7613,6 +8694,15 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-equals": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", + "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -8144,6 +9234,13 @@ "uglify-js": "^3.1.4" } }, + "node_modules/harmony-reflect": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", + "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==", + "dev": true, + "license": "(Apache-2.0 OR MPL-1.1)" + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -8306,6 +9403,19 @@ "node": ">=0.10.0" } }, + "node_modules/identity-obj-proxy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", + "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", + "dev": true, + "license": "MIT", + "dependencies": { + "harmony-reflect": "^1.4.6" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -8316,6 +9426,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", + "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -8392,6 +9512,16 @@ "dev": true, "license": "ISC" }, + "node_modules/input-otp": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz", + "integrity": "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -8407,6 +9537,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -9940,7 +11079,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -10371,6 +11509,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -10396,7 +11540,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -10737,6 +11880,16 @@ } } }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -10823,7 +11976,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11354,7 +12506,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -11425,6 +12576,27 @@ "node": ">=0.10.0" } }, + "node_modules/react-day-picker": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.9.0.tgz", + "integrity": "sha512-NtkJbuX6cl/VaGNb3sVVhmMA6LSMnL5G3xNL+61IyoZj0mUZFWTg4hmj7PHjIQ8MXN9dHWhUHFoJWG6y60DKSg==", + "license": "MIT", + "dependencies": { + "@date-fns/tz": "^1.4.1", + "date-fns": "^4.1.0", + "date-fns-jalali": "^4.1.0-0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/react-dom": { "version": "19.1.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", @@ -11437,13 +12609,60 @@ "react": "^19.1.1" } }, + "node_modules/react-hook-form": { + "version": "7.62.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz", + "integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-remove-scroll": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", @@ -11491,6 +12710,31 @@ } } }, + "node_modules/react-resizable-panels": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-3.0.5.tgz", + "integrity": "sha512-3z1yN25DMTXLg2wfyFrW32r5k4WEcUa3F7cJ2EgtNK07lnOs4mpM8yWLGunCpkhcQRwJX4fqoLcIh/pHPxzlmQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -11513,6 +12757,60 @@ } } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -11527,6 +12825,21 @@ "node": ">=8" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -11652,6 +12965,12 @@ "node": ">=0.10.0" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -12073,6 +13392,16 @@ "node": ">=8" } }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -12494,6 +13823,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swr": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz", + "integrity": "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -12611,6 +13953,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", @@ -13177,6 +14525,15 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -13192,6 +14549,41 @@ "node": ">=10.12.0" } }, + "node_modules/vaul": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", + "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -13625,6 +15017,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.5.tgz", + "integrity": "sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/client/package.json b/client/package.json index 8012cdd..d809c55 100644 --- a/client/package.json +++ b/client/package.json @@ -10,23 +10,61 @@ "test": "jest" }, "dependencies": { + "@hookform/resolvers": "^5.2.1", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-aspect-ratio": "^1.1.7", + "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-context-menu": "^2.2.16", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-menubar": "^1.1.16", + "@radix-ui/react-navigation-menu": "^1.2.14", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", + "@reduxjs/toolkit": "^2.9.0", "axios": "^1.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^4.1.0", + "embla-carousel-react": "^8.6.0", + "input-otp": "^1.4.2", "js-cookie": "^3.0.5", "lightningcss": "^1.30.1", "lucide-react": "^0.454.0", "next": "15.2.4", + "next-themes": "^0.4.6", "postcss": "^8.5", "react": "^19", + "react-day-picker": "^9.9.0", "react-dom": "^19", + "react-hook-form": "^7.62.0", + "react-icons": "^5.5.0", + "react-redux": "^9.2.0", + "react-resizable-panels": "^3.0.5", + "recharts": "^2.15.4", + "sonner": "^2.0.7", + "swr": "^2.3.6", "tailwind-merge": "^2.5.5", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "vaul": "^1.1.2", + "zod": "^4.1.5" }, "devDependencies": { "@babel/preset-env": "^7.28.3", @@ -47,6 +85,7 @@ "babel-jest": "^30.1.2", "eslint": "^9", "eslint-config-next": "15.4.7", + "identity-obj-proxy": "^3.0.0", "jest": "^30.1.2", "jest-environment-jsdom": "^30.1.2", "tailwindcss": "^4.1.9", diff --git a/client/src/app/auth/_components/SigninForm.test.tsx b/client/src/app/auth/_components/SigninForm.test.tsx new file mode 100644 index 0000000..d64cf30 --- /dev/null +++ b/client/src/app/auth/_components/SigninForm.test.tsx @@ -0,0 +1,282 @@ +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import SigninForm from "./SigninForm"; + +describe("SigninForm", () => { + const defaultProps = { + email: "", + password: "", + remember: false, + error: "", + loading: false, + onEmailChange: jest.fn(), + onPasswordChange: jest.fn(), + onRememberChange: jest.fn(), + onSubmit: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("Rendering", () => { + it("renders all form fields correctly", () => { + render(); + + expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/password/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/remember me/i)).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: /sign in/i }) + ).toBeInTheDocument(); + }); + + it("displays placeholders correctly", () => { + render(); + + expect( + screen.getByPlaceholderText("Enter your email") + ).toBeInTheDocument(); + expect( + screen.getByPlaceholderText("Enter your Password") + ).toBeInTheDocument(); + }); + + it("sets form fields as required", () => { + render(); + + const emailInput = screen.getByLabelText(/email/i); + const passwordInput = screen.getByLabelText(/password/i); + + expect(emailInput).toBeRequired(); + expect(passwordInput).toBeRequired(); + }); + + it("sets correct input types", () => { + render(); + + const emailInput = screen.getByLabelText(/email/i); + const passwordInput = screen.getByLabelText(/password/i); + + expect(emailInput).toHaveAttribute("type", "email"); + expect(passwordInput).toHaveAttribute("type", "password"); + }); + }); + + describe("User Interactions", () => { + it("calls onEmailChange when email input changes", async () => { + const user = userEvent.setup(); + render(); + + const emailInput = screen.getByLabelText(/email/i); + await user.type(emailInput, "test@example.com"); + + expect(defaultProps.onEmailChange).toHaveBeenCalled(); + }); + + it("calls onPasswordChange when password input changes", async () => { + const user = userEvent.setup(); + render(); + + const passwordInput = screen.getByLabelText(/password/i); + await user.type(passwordInput, "password123"); + + expect(defaultProps.onPasswordChange).toHaveBeenCalled(); + }); + + it("calls onRememberChange when remember me checkbox is clicked", async () => { + const user = userEvent.setup(); + render(); + + const rememberCheckbox = screen.getByLabelText(/remember me/i); + await user.click(rememberCheckbox); + + expect(defaultProps.onRememberChange).toHaveBeenCalled(); + }); + + it("calls onSubmit when form is submitted", async () => { + const user = userEvent.setup(); + const mockOnSubmit = jest.fn((e) => e.preventDefault()); + + render(); + + // Fill in required fields first + const emailInput = screen.getByLabelText(/email/i); + const passwordInput = screen.getByLabelText(/password/i); + + await user.type(emailInput, "test@example.com"); + await user.type(passwordInput, "password123"); + + // Submit the form directly + const form = screen.getByRole("form"); + fireEvent.submit(form); + + expect(mockOnSubmit).toHaveBeenCalled(); + }); + + it("prevents default form submission", () => { + render(); + + // Simulate form submission + fireEvent.submit( + screen.getByRole("form") || + screen.getByTestId("signin-form") || + document.querySelector("form")! + ); + + expect(defaultProps.onSubmit).toHaveBeenCalled(); + }); + }); + + describe("State Display", () => { + it("displays email value correctly", () => { + render(); + + const emailInput = screen.getByLabelText(/email/i); + expect(emailInput).toHaveValue("test@example.com"); + }); + + it("displays password value correctly", () => { + render(); + + const passwordInput = screen.getByLabelText(/password/i); + expect(passwordInput).toHaveValue("mypassword"); + }); + + it("displays remember me checked state correctly", () => { + render(); + + const rememberCheckbox = screen.getByLabelText(/remember me/i); + expect(rememberCheckbox).toBeChecked(); + }); + + it("displays remember me unchecked state correctly", () => { + render(); + + const rememberCheckbox = screen.getByLabelText(/remember me/i); + expect(rememberCheckbox).not.toBeChecked(); + }); + }); + + describe("Error Handling", () => { + it("displays error message when error prop is provided", () => { + const errorMessage = "Invalid credentials"; + render(); + + expect(screen.getByText(errorMessage)).toBeInTheDocument(); + expect(screen.getByText(errorMessage)).toHaveClass("text-red-500"); + }); + + it("does not display error message when error prop is empty", () => { + render(); + + const errorElements = screen.queryAllByText(/error|invalid|wrong/i); + expect(errorElements).toHaveLength(0); + }); + + it("applies correct error styling", () => { + const errorMessage = "Network error occurred"; + render(); + + const errorElement = screen.getByText(errorMessage); + expect(errorElement).toHaveClass( + "text-red-500", + "text-sm", + "text-center" + ); + }); + }); + + describe("Loading States", () => { + it("displays loading text when loading is true", () => { + render(); + + expect( + screen.getByRole("button", { name: /signing in/i }) + ).toBeInTheDocument(); + }); + + it("displays normal text when loading is false", () => { + render(); + + expect( + screen.getByRole("button", { name: /^sign in$/i }) + ).toBeInTheDocument(); + }); + + it("disables submit button when loading is true", () => { + render(); + + const submitButton = screen.getByRole("button", { name: /signing in/i }); + expect(submitButton).toBeDisabled(); + }); + + it("enables submit button when loading is false", () => { + render(); + + const submitButton = screen.getByRole("button", { name: /sign in/i }); + expect(submitButton).not.toBeDisabled(); + }); + }); + + describe("Form Validation", () => { + it("should trigger HTML5 validation for required fields", async () => { + render(); + + const submitButton = screen.getByRole("button", { name: /sign in/i }); + fireEvent.click(submitButton); + + // HTML5 validation should prevent submission of empty required fields + const emailInput = screen.getByLabelText(/email/i); + const passwordInput = screen.getByLabelText(/password/i); + + expect(emailInput).toBeInvalid(); + expect(passwordInput).toBeInvalid(); + }); + + it("should be valid when all required fields are filled", () => { + render( + + ); + + const emailInput = screen.getByLabelText(/email/i); + const passwordInput = screen.getByLabelText(/password/i); + + expect(emailInput).toBeValid(); + expect(passwordInput).toBeValid(); + }); + }); + + describe("Accessibility", () => { + it("has proper form structure", () => { + render(); + + const form = document.querySelector("form"); + expect(form).toBeInTheDocument(); + }); + + it("associates labels with inputs correctly", () => { + render(); + + const emailInput = screen.getByLabelText(/email/i); + const passwordInput = screen.getByLabelText(/password/i); + const rememberCheckbox = screen.getByLabelText(/remember me/i); + + expect(emailInput).toHaveAccessibleName(); + expect(passwordInput).toHaveAccessibleName(); + expect(rememberCheckbox).toHaveAccessibleName(); + }); + + it("has proper button semantics", () => { + render(); + + const submitButton = screen.getByRole("button", { name: /sign in/i }); + expect(submitButton).toHaveAttribute("type", "submit"); + }); + }); +}); diff --git a/client/src/app/auth/_components/SigninForm.tsx b/client/src/app/auth/_components/SigninForm.tsx index a2fa07c..2816549 100644 --- a/client/src/app/auth/_components/SigninForm.tsx +++ b/client/src/app/auth/_components/SigninForm.tsx @@ -2,7 +2,6 @@ import React from "react"; import Input from "@/components/shared/Input"; import Button from "@/components/Button"; import Checkbox from "@/components/shared/Checkbox"; -import Divider from "@/components/shared/Divider"; interface SigninFormProps { email: string; @@ -27,7 +26,12 @@ const SigninForm: React.FC = ({ onRememberChange, onSubmit, }) => ( -
+ = ({ - Or continue with - {/* Google Button and other options can be added here if needed */}
{ + const defaultProps = { + fullName: "", + email: "", + businessName: "", + password: "", + confirmPassword: "", + agree: false, + error: "", + loading: false, + onFullNameChange: jest.fn(), + onEmailChange: jest.fn(), + onBusinessNameChange: jest.fn(), + onPasswordChange: jest.fn(), + onConfirmPasswordChange: jest.fn(), + onAgreeChange: jest.fn(), + onSubmit: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("Rendering", () => { + it("renders all form fields correctly", () => { + render(); + + expect(screen.getByLabelText(/full name/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/^email$/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/business name/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/^password$/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/confirm password/i)).toBeInTheDocument(); + expect( + screen.getByLabelText(/i agree to the terms/i) + ).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: /create account/i }) + ).toBeInTheDocument(); + }); + + it("displays placeholders correctly", () => { + render(); + + expect(screen.getByPlaceholderText("John Doe")).toBeInTheDocument(); + expect( + screen.getByPlaceholderText("Enter your email") + ).toBeInTheDocument(); + expect( + screen.getByPlaceholderText("Enter your Business Name") + ).toBeInTheDocument(); + expect( + screen.getByPlaceholderText("Create a password") + ).toBeInTheDocument(); + expect( + screen.getByPlaceholderText("Confirm your password") + ).toBeInTheDocument(); + }); + + it("sets form fields as required", () => { + render(); + + const fullNameInput = screen.getByLabelText(/full name/i); + const emailInput = screen.getByLabelText(/^email$/i); + const businessNameInput = screen.getByLabelText(/business name/i); + const passwordInput = screen.getByLabelText(/^password$/i); + const confirmPasswordInput = screen.getByLabelText(/confirm password/i); + + expect(fullNameInput).toBeRequired(); + expect(emailInput).toBeRequired(); + expect(businessNameInput).toBeRequired(); + expect(passwordInput).toBeRequired(); + expect(confirmPasswordInput).toBeRequired(); + }); + + it("sets correct input types", () => { + render(); + + const emailInput = screen.getByLabelText(/^email$/i); + const passwordInput = screen.getByLabelText(/^password$/i); + const confirmPasswordInput = screen.getByLabelText(/confirm password/i); + + expect(emailInput).toHaveAttribute("type", "email"); + expect(passwordInput).toHaveAttribute("type", "password"); + expect(confirmPasswordInput).toHaveAttribute("type", "password"); + }); + }); + + describe("User Interactions", () => { + it("calls onFullNameChange when full name input changes", async () => { + const user = userEvent.setup(); + render(); + + const fullNameInput = screen.getByPlaceholderText("John Doe"); + await user.type(fullNameInput, "John Doe"); + + expect(defaultProps.onFullNameChange).toHaveBeenCalled(); + }); + + it("calls onEmailChange when email input changes", async () => { + const user = userEvent.setup(); + render(); + + const emailInput = screen.getByPlaceholderText("Enter your email"); + await user.type(emailInput, "test@example.com"); + + expect(defaultProps.onEmailChange).toHaveBeenCalled(); + }); + + it("calls onBusinessNameChange when business name input changes", async () => { + const user = userEvent.setup(); + render(); + + const businessNameInput = screen.getByPlaceholderText( + "Enter your Business Name" + ); + await user.type(businessNameInput, "My Business"); + + expect(defaultProps.onBusinessNameChange).toHaveBeenCalled(); + }); + + it("calls onPasswordChange when password input changes", async () => { + const user = userEvent.setup(); + render(); + + const passwordInput = screen.getByPlaceholderText("Create a password"); + await user.type(passwordInput, "password123"); + + expect(defaultProps.onPasswordChange).toHaveBeenCalled(); + }); + + it("calls onConfirmPasswordChange when confirm password input changes", async () => { + const user = userEvent.setup(); + render(); + + const confirmPasswordInput = screen.getByPlaceholderText( + "Confirm your password" + ); + await user.type(confirmPasswordInput, "password123"); + + expect(defaultProps.onConfirmPasswordChange).toHaveBeenCalled(); + }); + + it("calls onAgreeChange when terms checkbox is clicked", async () => { + const user = userEvent.setup(); + render(); + + const agreeCheckbox = screen.getByLabelText(/i agree to the terms/i); + await user.click(agreeCheckbox); + + expect(defaultProps.onAgreeChange).toHaveBeenCalled(); + }); + + it("calls onSubmit when form is submitted", async () => { + const user = userEvent.setup(); + const mockOnSubmit = jest.fn((e) => e.preventDefault()); + + render(); + + // Fill in required fields first + const fullNameInput = screen.getByLabelText(/full name/i); + const emailInput = screen.getByLabelText(/^email$/i); + const businessNameInput = screen.getByLabelText(/business name/i); + const passwordInput = screen.getByLabelText(/^password$/i); + const confirmPasswordInput = screen.getByLabelText(/confirm password/i); + const agreeCheckbox = screen.getByLabelText(/terms of services/i); + + await user.type(fullNameInput, "John Doe"); + await user.type(emailInput, "test@example.com"); + await user.type(businessNameInput, "Test Business"); + await user.type(passwordInput, "password123"); + await user.type(confirmPasswordInput, "password123"); + await user.click(agreeCheckbox); + + // Submit the form directly + const form = screen.getByRole("form"); + fireEvent.submit(form); + + expect(mockOnSubmit).toHaveBeenCalled(); + }); + + it("prevents default form submission", () => { + render(); + + // Simulate form submission + fireEvent.submit(document.querySelector("form")!); + + expect(defaultProps.onSubmit).toHaveBeenCalled(); + }); + }); + + describe("State Display", () => { + it("displays all field values correctly", () => { + const props = { + ...defaultProps, + fullName: "John Doe", + email: "john@example.com", + businessName: "Doe Enterprises", + password: "mypassword", + confirmPassword: "mypassword", + agree: true, + }; + + render(); + + expect(screen.getByPlaceholderText("John Doe")).toHaveValue("John Doe"); + expect(screen.getByPlaceholderText("Enter your email")).toHaveValue( + "john@example.com" + ); + expect( + screen.getByPlaceholderText("Enter your Business Name") + ).toHaveValue("Doe Enterprises"); + expect(screen.getByPlaceholderText("Create a password")).toHaveValue( + "mypassword" + ); + expect(screen.getByPlaceholderText("Confirm your password")).toHaveValue( + "mypassword" + ); + expect(screen.getByLabelText(/i agree to the terms/i)).toBeChecked(); + }); + + it("displays unchecked agreement checkbox correctly", () => { + render(); + + const agreeCheckbox = screen.getByLabelText(/i agree to the terms/i); + expect(agreeCheckbox).not.toBeChecked(); + }); + }); + + describe("Error Handling", () => { + it("displays error message when error prop is provided", () => { + const errorMessage = "Email already exists"; + render(); + + expect(screen.getByText(errorMessage)).toBeInTheDocument(); + }); + + it("does not display error message when error prop is empty", () => { + render(); + + const errorElements = screen.queryAllByText(/error|invalid|exists/i); + expect(errorElements).toHaveLength(0); + }); + + it("applies correct error styling", () => { + const errorMessage = "Passwords do not match"; + render(); + + const errorElement = screen.getByText(errorMessage); + // Check if it has the CSS module class (we can't test the exact class name due to CSS modules) + expect(errorElement).toBeInTheDocument(); + }); + }); + + describe("Loading States", () => { + it("displays loading text when loading is true", () => { + render(); + + expect( + screen.getByRole("button", { name: /creating account/i }) + ).toBeInTheDocument(); + }); + + it("displays normal text when loading is false", () => { + render(); + + expect( + screen.getByRole("button", { name: /^create account$/i }) + ).toBeInTheDocument(); + }); + + it("disables submit button when loading is true", () => { + render(); + + const submitButton = screen.getByRole("button", { + name: /creating account/i, + }); + expect(submitButton).toBeDisabled(); + }); + + it("enables submit button when loading is false", () => { + render(); + + const submitButton = screen.getByRole("button", { + name: /create account/i, + }); + expect(submitButton).not.toBeDisabled(); + }); + }); + + describe("Form Validation", () => { + it("should trigger HTML5 validation for required fields", async () => { + render(); + + const fullNameInput = screen.getByPlaceholderText("John Doe"); + const emailInput = screen.getByPlaceholderText("Enter your email"); + const businessNameInput = screen.getByPlaceholderText( + "Enter your Business Name" + ); + const passwordInput = screen.getByPlaceholderText("Create a password"); + const confirmPasswordInput = screen.getByPlaceholderText( + "Confirm your password" + ); + + expect(fullNameInput).toBeInvalid(); + expect(emailInput).toBeInvalid(); + expect(businessNameInput).toBeInvalid(); + expect(passwordInput).toBeInvalid(); + expect(confirmPasswordInput).toBeInvalid(); + }); + + it("should be valid when all required fields are filled with valid data", () => { + const props = { + ...defaultProps, + fullName: "John Doe", + email: "john@example.com", + businessName: "Doe Enterprises", + password: "password123", + confirmPassword: "password123", + }; + + render(); + + const fullNameInput = screen.getByPlaceholderText("John Doe"); + const emailInput = screen.getByPlaceholderText("Enter your email"); + const businessNameInput = screen.getByPlaceholderText( + "Enter your Business Name" + ); + const passwordInput = screen.getByPlaceholderText("Create a password"); + const confirmPasswordInput = screen.getByPlaceholderText( + "Confirm your password" + ); + + expect(fullNameInput).toBeValid(); + expect(emailInput).toBeValid(); + expect(businessNameInput).toBeValid(); + expect(passwordInput).toBeValid(); + expect(confirmPasswordInput).toBeValid(); + }); + + it("validates email format", () => { + render(); + + const emailInput = screen.getByPlaceholderText("Enter your email"); + expect(emailInput).toBeInvalid(); + }); + }); + + describe("Password Fields", () => { + it("masks password inputs", () => { + render(); + + const passwordInput = screen.getByPlaceholderText("Create a password"); + const confirmPasswordInput = screen.getByPlaceholderText( + "Confirm your password" + ); + + expect(passwordInput).toHaveAttribute("type", "password"); + expect(confirmPasswordInput).toHaveAttribute("type", "password"); + }); + }); + + describe("Terms and Conditions", () => { + it("renders terms of service agreement", () => { + render(); + + expect( + screen.getByLabelText( + /i agree to the terms of services and privacy policy/i + ) + ).toBeInTheDocument(); + }); + + it("allows checking and unchecking the agreement", async () => { + const user = userEvent.setup(); + render(); + + const agreeCheckbox = screen.getByLabelText(/i agree to the terms/i); + + // Initially unchecked + expect(agreeCheckbox).not.toBeChecked(); + + // Click to check + await user.click(agreeCheckbox); + expect(defaultProps.onAgreeChange).toHaveBeenCalled(); + }); + }); + + describe("Accessibility", () => { + it("has proper form structure", () => { + render(); + + const form = document.querySelector("form"); + expect(form).toBeInTheDocument(); + }); + + it("has proper input accessibility", () => { + render(); + + const fullNameInput = screen.getByPlaceholderText("John Doe"); + const emailInput = screen.getByPlaceholderText("Enter your email"); + const businessNameInput = screen.getByPlaceholderText( + "Enter your Business Name" + ); + const passwordInput = screen.getByPlaceholderText("Create a password"); + const confirmPasswordInput = screen.getByPlaceholderText( + "Confirm your password" + ); + const agreeCheckbox = screen.getByLabelText(/i agree to the terms/i); + + expect(fullNameInput).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(businessNameInput).toBeInTheDocument(); + expect(passwordInput).toBeInTheDocument(); + expect(confirmPasswordInput).toBeInTheDocument(); + expect(agreeCheckbox).toHaveAccessibleName(); + }); + + it("has proper button semantics", () => { + render(); + + const submitButton = screen.getByRole("button", { + name: /create account/i, + }); + expect(submitButton).toHaveAttribute("type", "submit"); + }); + }); + + describe("CSS Modules Integration", () => { + it("applies CSS module classes", () => { + render(); + + const form = document.querySelector("form"); + expect(form).toHaveClass("signupForm"); + }); + }); +}); diff --git a/client/src/app/auth/_components/SignupForm.tsx b/client/src/app/auth/_components/SignupForm.tsx index 78ed32a..1d73710 100644 --- a/client/src/app/auth/_components/SignupForm.tsx +++ b/client/src/app/auth/_components/SignupForm.tsx @@ -39,7 +39,12 @@ const SignupForm: React.FC = ({ onAgreeChange, onSubmit, }) => ( - + { const [loading, setLoading] = useState(false); const router = useRouter(); + // Redirect if already authenticated + useRedirectIfAuthenticated(); + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(""); @@ -26,9 +43,16 @@ const SigninContainer: React.FC = () => { setLoading(true); try { await csrf(); - await api.post("/v1/login", { email, password }); + const response = await api.post("/v1/login", { + email, + password, + }); setLoading(false); - router.push("/Business-setup"); + // Save user name to localStorage if available in response + if (response.data.data.user.name) { + localStorage.setItem("user_name", response.data.data.user.name); + } + router.push("/business-hub"); } catch (err: unknown) { setLoading(false); setError(getErrorMessage(err, "Login failed.")); @@ -70,19 +94,6 @@ const SigninContainer: React.FC = () => { onRememberChange={(e) => setRemember(e.target.checked)} onSubmit={handleSubmit} /> - {/* Divider and Google Button */} - {/* Options Row */}
diff --git a/client/src/app/auth/containers/SignupContainer.tsx b/client/src/app/auth/containers/SignupContainer.tsx index aa529bd..5c07dbe 100644 --- a/client/src/app/auth/containers/SignupContainer.tsx +++ b/client/src/app/auth/containers/SignupContainer.tsx @@ -2,6 +2,7 @@ import React, { useState } from "react"; import api, { csrf } from "@/lib/api"; import { getErrorMessage } from "@/utils/errorMessage"; +import { useRedirectIfAuthenticated } from "@/hooks/useRedirectIfAuthenticated"; import SignupForm from "../_components/SignupForm"; import Link from "next/link"; import Image from "next/image"; @@ -17,6 +18,9 @@ const SignupContainer: React.FC = () => { const [error, setError] = useState(""); const [loading, setLoading] = useState(false); + // Redirect if already authenticated + useRedirectIfAuthenticated(); + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(""); diff --git a/client/src/app/auth/google/callback/page.tsx b/client/src/app/auth/google/callback/page.tsx deleted file mode 100644 index cb6eca6..0000000 --- a/client/src/app/auth/google/callback/page.tsx +++ /dev/null @@ -1,29 +0,0 @@ -"use client"; -import { useEffect } from "react"; -import { useRouter } from "next/navigation"; - -export default function GoogleCallbackPage() { - const router = useRouter(); - - useEffect(() => { - // After Google OAuth, the backend should redirect here with token/user info - // This example assumes the backend redirects with query params: ?token=...&user=... - // Adjust parsing as needed for your backend's actual response - const params = new URLSearchParams(window.location.search); - const token = params.get("token"); - // Optionally, parse user info if provided - // const user = params.get("user"); - - if (token) { - // Store token (localStorage, cookie, or context) - localStorage.setItem("auth_token", token); - // Redirect to dashboard or home - router.replace("/dashboard"); - } else { - // Handle error or fallback - router.replace("/auth/signin?error=google_auth_failed"); - } - }, [router]); - - return
Signing you in with Google...
; -} diff --git a/client/src/app/auth/page.tsx b/client/src/app/auth/page.tsx index fc685b7..b60f38b 100644 --- a/client/src/app/auth/page.tsx +++ b/client/src/app/auth/page.tsx @@ -1,6 +1,6 @@ import { redirect } from "next/navigation"; export default function AuthPage() { - redirect("/auth/login"); + redirect("/auth/signin"); return null; } diff --git a/client/src/app/auth/signin/page.module.css b/client/src/app/auth/signin/page.module.css index 65b4776..e2c1268 100644 --- a/client/src/app/auth/signin/page.module.css +++ b/client/src/app/auth/signin/page.module.css @@ -1,29 +1,6 @@ - .googleBtn { - width: 100%; - max-width: 28rem; - border: 2px solid var(--color-primary); - border-radius: 0.5rem; - padding: 0.5rem 0; - display: flex; - align-items: center; - justify-content: center; - font-weight: bold; - font-size: 1.125rem; - color: var(--color-primary); - background: var(--color-white); - margin-bottom: 0.5rem; - transition: background 0.2s, color 0.2s, border-color 0.2s; - cursor: pointer; -} - .googleBtn:hover { - background: var(--color-secondary); - border-color: var(--color-secondary); - color: var(--color-primary); -} - .googleIcon { - margin-right: 0.5rem; - font-size: 1.25rem; -} + .slotifyText { + font-weight: bold; + } .calendarIcon { margin-bottom: 3.5rem; } @@ -47,14 +24,15 @@ flex: 1; align-items: center; justify-content: center; - padding: 2rem 3rem; + padding: 0.75rem 1.25rem; + max-height: 100vh; } .logoContainer { display: flex; flex-direction: column; align-items: center; - margin-bottom: 1.5rem; - margin-top: 2.5rem; + margin-bottom: 0.5rem; + margin-top: 1rem; } .logoCircle { width: 112px; diff --git a/client/src/app/business-dashboard/[businessId]/clients/components/add-client-modal.tsx b/client/src/app/business-dashboard/[businessId]/clients/components/add-client-modal.tsx new file mode 100644 index 0000000..df7101a --- /dev/null +++ b/client/src/app/business-dashboard/[businessId]/clients/components/add-client-modal.tsx @@ -0,0 +1,170 @@ +import React from "react"; +import { mutate } from "swr"; +import { createClient } from "@/lib/clientsAPI"; + +interface AddClientModalProps { + open: boolean; + onClose: () => void; + businessId: number; +} + +export default function AddClientModal({ + open, + onClose, + businessId, +}: AddClientModalProps) { + const [name, setName] = React.useState(""); + const [phone, setPhone] = React.useState(""); + const [email, setEmail] = React.useState(""); + const [error, setError] = React.useState(null); + const [validationErrors, setValidationErrors] = React.useState< + Record + >({}); + const [loading, setLoading] = React.useState(false); + + // Reset form when modal opens + React.useEffect(() => { + if (open) { + setName(""); + setPhone(""); + setEmail(""); + setError(null); + setValidationErrors({}); + setLoading(false); + } + }, [open]); + + function validate() { + const errors: Record = {}; + if (!name.trim()) errors.name = "Name is required."; + if (!phone.trim()) errors.phone = "Phone is required."; + setValidationErrors(errors); + return Object.keys(errors).length === 0; + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + if (!validate()) return; + setLoading(true); + try { + await createClient(businessId, { name, phone, email }); + // Refresh client list after adding + mutate([`/businesses/${businessId}/clients`, businessId]); + setLoading(false); + onClose(); + } catch (err: unknown) { + setLoading(false); + if (err && typeof err === "object" && "message" in err) { + setError( + (err as { message?: string }).message || "Failed to add client." + ); + } else { + setError("Failed to add client."); + } + } + } + + if (!open) return null; + + return ( +
+
+ {/* Close button */} + + +

+ Add New Client +

+ + {error && ( +
{error}
+ )} + + + {/* Name */} +
+ + setName(e.target.value)} + required + placeholder="Enter client name" + /> + {validationErrors.name && ( +
+ {validationErrors.name} +
+ )} +
+ {/* Phone */} +
+ + setPhone(e.target.value)} + required + placeholder="Enter phone number" + /> + {validationErrors.phone && ( +
+ {validationErrors.phone} +
+ )} +
+ {/* Email */} +
+ + setEmail(e.target.value)} + placeholder="Enter email (optional)" + /> +
+ {/* Actions */} +
+ + +
+ +
+
+ ); +} diff --git a/client/src/app/business-dashboard/[businessId]/clients/components/client-filters.tsx b/client/src/app/business-dashboard/[businessId]/clients/components/client-filters.tsx new file mode 100644 index 0000000..e1d9f3c --- /dev/null +++ b/client/src/app/business-dashboard/[businessId]/clients/components/client-filters.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { Input } from "@/components/ui/input"; +import { Search } from "lucide-react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +export function ClientFilters({ + searchQuery, + setSearchQuery, + dateRange, + setDateRange, + sortBy, + setSortBy, +}: { + searchQuery: string; + setSearchQuery: (q: string) => void; + dateRange: string; + setDateRange: (r: string) => void; + sortBy: string; + setSortBy: (s: string) => void; +}) { + return ( +
+
+ + setSearchQuery(e.target.value)} + className="pl-10" + /> +
+ +
Sort by:
+ +
+ ); +} diff --git a/client/src/app/business-dashboard/[businessId]/clients/components/client-list.tsx b/client/src/app/business-dashboard/[businessId]/clients/components/client-list.tsx new file mode 100644 index 0000000..02c28e1 --- /dev/null +++ b/client/src/app/business-dashboard/[businessId]/clients/components/client-list.tsx @@ -0,0 +1,130 @@ +"use client"; + +import { Card, CardContent } from "@/components/ui/card"; +import useSWR from "swr"; +import { fetchClients, Client } from "@/lib/clientsAPI"; + +export function ClientList({ + businessId, + searchQuery, + dateRange, + sortBy, +}: { + businessId: number; + searchQuery: string; + dateRange: string; + sortBy: string; +}) { + const { + data: clients = [], + error, + isLoading, + } = useSWR([`/businesses/${businessId}/clients`, businessId], () => + fetchClients(businessId) + ); + + // Date filter logic + const now = new Date(); + function isInRange(dateStr: string | null | undefined) { + if (!dateStr) return dateRange === "all-time"; + const date = new Date(dateStr); + switch (dateRange) { + case "day": + return now.getTime() - date.getTime() <= 24 * 60 * 60 * 1000; + case "week": + return now.getTime() - date.getTime() <= 7 * 24 * 60 * 60 * 1000; + case "month": + return now.getTime() - date.getTime() <= 30 * 24 * 60 * 60 * 1000; + case "all-time": + default: + return true; + } + } + + let filteredClients = clients.filter((client) => { + const q = searchQuery.toLowerCase(); + const matchesSearch = + client.name.toLowerCase().includes(q) || + (client.email && client.email.toLowerCase().includes(q)) || + client.phone.toLowerCase().includes(q); + const matchesDate = isInRange(client.last_whatsapp_activity); + return matchesSearch && matchesDate; + }); + + // Sorting logic + if (sortBy === "last-activity") { + filteredClients = filteredClients.sort((a, b) => { + const aDate = a.last_whatsapp_activity + ? new Date(a.last_whatsapp_activity).getTime() + : 0; + const bDate = b.last_whatsapp_activity + ? new Date(b.last_whatsapp_activity).getTime() + : 0; + return bDate - aDate; + }); + } else if (sortBy === "name") { + filteredClients = filteredClients.sort((a, b) => + a.name.localeCompare(b.name) + ); + } else if (sortBy === "bookings") { + filteredClients = filteredClients.sort( + (a, b) => b.bookings.length - a.bookings.length + ); + } + + return ( + <> + + +
+
+
Name
+
Phone
+
Email
+
WhatsApp Activity
+
Total Bookings
+
+
+ {isLoading ? ( +
+ Loading clients... +
+ ) : error ? ( +
+ Failed to load clients +
+ ) : filteredClients.length === 0 ? ( +
+ No clients found. +
+ ) : ( + filteredClients.map((client: Client) => ( +
+
+
{client.name}
+
+ {client.phone} +
+
+ {client.email || "-"} +
+
+ {client.last_whatsapp_activity + ? new Date(client.last_whatsapp_activity).toLocaleString() + : "-"} +
+
+ {client.bookings.length} +
+
+
+ )) + )} +
+
+ + ); +} diff --git a/client/src/app/business-dashboard/[businessId]/clients/components/client-stats.tsx b/client/src/app/business-dashboard/[businessId]/clients/components/client-stats.tsx new file mode 100644 index 0000000..749b31a --- /dev/null +++ b/client/src/app/business-dashboard/[businessId]/clients/components/client-stats.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { Card, CardContent } from "@/components/ui/card"; + +export function ClientStats() { + return ( + + +

Quick Stats

+
+
+ Total Clients + 247 +
+
+ Active This Month + 189 +
+
+ New Clients + +12 +
+
+ No-show Rate + 8.2% +
+
+
+
+ ); +} diff --git a/client/src/app/business-dashboard/[businessId]/clients/containers/client-container.tsx b/client/src/app/business-dashboard/[businessId]/clients/containers/client-container.tsx new file mode 100644 index 0000000..cd84153 --- /dev/null +++ b/client/src/app/business-dashboard/[businessId]/clients/containers/client-container.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { useParams } from "next/navigation"; +import { useState, useEffect } from "react"; +import { ClientFilters } from "../components/client-filters"; +import { ClientList } from "../components/client-list"; +import AddClientModal from "../components/add-client-modal"; +import { Plus } from "lucide-react"; + +export function ClientContainer() { + const params = useParams(); + const businessId = Number(params.businessId); + const [isAddClientModalOpen, setIsAddClientModalOpen] = useState(false); + const [searchInput, setSearchInput] = useState(""); + const [searchQuery, setSearchQuery] = useState(""); + const [dateRange, setDateRange] = useState("all-time"); + const [sortBy, setSortBy] = useState("last-activity"); + + useEffect(() => { + const handler = setTimeout(() => { + setSearchQuery(searchInput); + }, 500); + return () => clearTimeout(handler); + }, [searchInput]); + return ( +
+ {/* Header */} +
+
+
+

+ Client Management +

+
+ + 247 Total Clients + +
+
+
+
+ +
+
+ +
+ + +
+ + setIsAddClientModalOpen(false)} + businessId={businessId} + /> +
+ ); +} diff --git a/client/src/app/business-dashboard/[businessId]/clients/page.tsx b/client/src/app/business-dashboard/[businessId]/clients/page.tsx new file mode 100644 index 0000000..b62da9d --- /dev/null +++ b/client/src/app/business-dashboard/[businessId]/clients/page.tsx @@ -0,0 +1,5 @@ +import { ClientContainer } from "./containers/client-container"; + +export default function ClientsPage() { + return ; +} diff --git a/client/src/app/business-dashboard/[businessId]/dashboard/components/appointments-table.tsx b/client/src/app/business-dashboard/[businessId]/dashboard/components/appointments-table.tsx new file mode 100644 index 0000000..2504a1e --- /dev/null +++ b/client/src/app/business-dashboard/[businessId]/dashboard/components/appointments-table.tsx @@ -0,0 +1,342 @@ +"use client"; + +import { useState } from "react"; +import useSWR from "swr"; +import { + fetchAppointments, + fetchAllAppointments, + Appointment, +} from "@/lib/appointmentsAPI"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Badge } from "@/components/ui/badge"; +import { ChevronLeft, ChevronRight, Calendar, Filter } from "lucide-react"; + +export function AppointmentsTable({ businessId }: { businessId: number }) { + const [timeFilter, setTimeFilter] = useState("week"); + const [serviceFilter, setServiceFilter] = useState("all"); + const [clientFilter, setClientFilter] = useState("all"); + const [sortOrder, setSortOrder] = useState("newest"); + const [currentPage, setCurrentPage] = useState(1); + const itemsPerPage = 5; + + // Create SWR key based on timeFilter + const swrKey = + timeFilter === "all" || timeFilter === "week" || timeFilter === "month" + ? [`/businesses/${businessId}/appointments/all`, businessId] + : [`/businesses/${businessId}/appointments`, businessId, timeFilter]; + + // SWR data fetching based on time filter with auto-refresh + const { + data: appointments = [], + error, + isLoading: loading, + } = useSWR( + swrKey, + () => { + if ( + timeFilter === "all" || + timeFilter === "week" || + timeFilter === "month" + ) { + return fetchAllAppointments(businessId); + } else { + let params = {}; + if (timeFilter === "today") { + const today = new Date().toISOString().slice(0, 10); + params = { date: today }; + } + return fetchAppointments(businessId, params); + } + }, + { + refreshInterval: 30000, // Auto-refresh every 30 seconds + refreshWhenHidden: false, // Don't refresh when tab is hidden + refreshWhenOffline: false, // Don't refresh when offline + } + ); + + // Get unique services and clients for filter options + const uniqueServices = Array.from( + new Set(appointments.map((apt) => apt.service.name)) + ); + const uniqueClients = Array.from( + new Set(appointments.map((apt) => apt.client.name)) + ); + + // Filter appointments based on selected filters + const filteredAppointments = appointments.filter((appointment) => { + const serviceMatch = + serviceFilter === "all" || appointment.service.name === serviceFilter; + const clientMatch = + clientFilter === "all" || appointment.client.name === clientFilter; + + // Time filter logic (frontend for week/month/all) + let timeMatch = true; + const appointmentDate = new Date(appointment.start_time); + const now = new Date(); + if (timeFilter === "week") { + // Get Monday of current week + const dayOfWeek = now.getDay(); // 0 (Sun) - 6 (Sat) + const mondayOffset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek; + const monday = new Date(now); + monday.setDate(now.getDate() + mondayOffset); + monday.setHours(0, 0, 0, 0); + const sunday = new Date(monday); + sunday.setDate(monday.getDate() + 6); + sunday.setHours(23, 59, 59, 999); + timeMatch = appointmentDate >= monday && appointmentDate <= sunday; + } else if (timeFilter === "month") { + // Get first and last day of current month + const firstDay = new Date(now.getFullYear(), now.getMonth(), 1); + const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0); + lastDay.setHours(23, 59, 59, 999); + timeMatch = appointmentDate >= firstDay && appointmentDate <= lastDay; + } else if (timeFilter === "today") { + timeMatch = appointmentDate.toDateString() === now.toDateString(); + } // 'all' returns all + + return serviceMatch && clientMatch && timeMatch; + }); + + // Sort appointments based on selected sort order + const sortedAppointments = [...filteredAppointments].sort((a, b) => { + const dateA = new Date(a.start_time).getTime(); + const dateB = new Date(b.start_time).getTime(); + + if (sortOrder === "newest") { + return dateB - dateA; // Newest first (descending) + } else { + return dateA - dateB; // Oldest first (ascending) + } + }); + + // Pagination + const totalPages = Math.ceil(sortedAppointments.length / itemsPerPage); + const startIndex = (currentPage - 1) * itemsPerPage; + const paginatedAppointments = sortedAppointments.slice( + startIndex, + startIndex + itemsPerPage + ); + + const formatTime = (dateString: string) => { + return new Date(dateString).toLocaleTimeString("en-US", { + hour: "2-digit", + minute: "2-digit", + }); + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); + }; + + const getStatusColor = (status: string) => { + switch (status) { + case "confirmed": + return "bg-green-100 text-green-800 border-green-200"; + case "completed": + return "bg-blue-100 text-blue-800 border-blue-200"; + case "cancelled": + return "bg-red-100 text-red-800 border-red-200"; + case "reassigned": + return "bg-yellow-100 text-yellow-800 border-yellow-200"; + default: + return "bg-gray-100 text-gray-800 border-gray-200"; + } + }; + + return ( + + +
+ + + Appointments + +
+ + Filters +
+
+ + {/* Filters */} +
+ + + + + + + +
+
+ + + {loading ? ( +
+ Loading appointments... +
+ ) : error ? ( +
+ Failed to fetch appointments +
+ ) : ( +
+ {paginatedAppointments.length === 0 ? ( +
+ No appointments found for the selected filters. +
+ ) : ( + paginatedAppointments.map((appointment) => ( +
+
+
+
+ {formatDate(appointment.start_time)} +
+
+ {formatTime(appointment.start_time)} -{" "} + {formatTime(appointment.end_time)} +
+
+ +
+ +
+
+ {appointment.client.name} +
+
+ {appointment.client.phone} +
+
+ +
+ +
+
+ {appointment.service.name} +
+
+ ${appointment.service.price} +
+
+ +
+ +
+
+ {appointment.resource?.name || "Unassigned"} +
+
+ Staff Member +
+
+
+ + + {appointment.status.charAt(0).toUpperCase() + + appointment.status.slice(1)} + +
+ )) + )} +
+ )} + + {/* Pagination */} + {totalPages > 1 && ( +
+
+ Showing {startIndex + 1} to{" "} + {Math.min(startIndex + itemsPerPage, sortedAppointments.length)}{" "} + of {sortedAppointments.length} appointments +
+ +
+ + + + Page {currentPage} of {totalPages} + + + +
+
+ )} + + + ); +} diff --git a/client/src/app/business-dashboard/[businessId]/dashboard/components/dashboard-header.tsx b/client/src/app/business-dashboard/[businessId]/dashboard/components/dashboard-header.tsx new file mode 100644 index 0000000..6c6cb21 --- /dev/null +++ b/client/src/app/business-dashboard/[businessId]/dashboard/components/dashboard-header.tsx @@ -0,0 +1,22 @@ +"use client"; +import { Button } from "@/components/ui/button"; +import { Users } from "lucide-react"; + +export function DashboardHeader({ onAddClient }: { onAddClient: () => void }) { + return ( +
+
+

Dashboard

+

+ Welcome back! Here is what is happening with your business today. +

+
+
+ +
+
+ ); +} diff --git a/client/src/app/business-dashboard/[businessId]/dashboard/components/schedule-card.tsx b/client/src/app/business-dashboard/[businessId]/dashboard/components/schedule-card.tsx new file mode 100644 index 0000000..8041c87 --- /dev/null +++ b/client/src/app/business-dashboard/[businessId]/dashboard/components/schedule-card.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Clock } from "lucide-react"; +import { useState } from "react"; +import { + Select, + SelectTrigger, + SelectValue, + SelectContent, + SelectItem, +} from "@/components/ui/select"; + +const upcomingAppointments = [ + { + time: "9:00 AM", + client: "Sarah Johnson", + service: "Haircut & Styling", + duration: "60 min", + }, + { + time: "11:30 AM", + client: "Mike Chen", + service: "Beard Trim", + duration: "30 min", + }, + { + time: "2:00 PM", + client: "Emma Davis", + service: "Hair Color", + duration: "90 min", + }, + { + time: "4:30 PM", + client: "Alex Rodriguez", + service: "Haircut", + duration: "45 min", + }, +]; + +const FILTER_OPTIONS = [ + { label: "Today", value: "today" }, + { label: "Last Week", value: "week" }, + { label: "Last Month", value: "month" }, + { label: "All Time", value: "all" }, +]; + +export function ScheduleCard() { + const [filter, setFilter] = useState("today"); + return ( + + +
+ + + Today's Schedule + + +
+
+ +
+ {upcomingAppointments.map((appointment, index) => ( +
+
+
+ {appointment.time} +
+
+
+ {appointment.client} +
+
+ {appointment.service} +
+
+
+
+ {appointment.duration} +
+
+ ))} +
+
+
+ ); +} diff --git a/client/src/app/business-dashboard/[businessId]/dashboard/components/stats-grid.tsx b/client/src/app/business-dashboard/[businessId]/dashboard/components/stats-grid.tsx new file mode 100644 index 0000000..568f671 --- /dev/null +++ b/client/src/app/business-dashboard/[businessId]/dashboard/components/stats-grid.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Users, CalendarDays } from "lucide-react"; + +type StatsGridProps = { + totalClients: number; + totalBookings: number; +}; + +export function StatsGrid({ totalClients, totalBookings }: StatsGridProps) { + const stats = [ + { + title: "Total Clients", + value: (totalClients ?? 0).toString(), + change: "", + icon: Users, + color: "text-blue-600", + }, + { + title: "Today's Bookings", + value: (totalBookings ?? 0).toString(), + change: "", + icon: CalendarDays, + color: "text-green-600", + }, + ]; + + return ( +
+ {stats.map((stat, index) => { + const Icon = stat.icon; + return ( + + + + {stat.title} + + + + +
+ {stat.value} +
+

{stat.change}

+
+
+ ); + })} +
+ ); +} diff --git a/client/src/app/business-dashboard/[businessId]/dashboard/containers/dashboard-container.tsx b/client/src/app/business-dashboard/[businessId]/dashboard/containers/dashboard-container.tsx new file mode 100644 index 0000000..b5aa681 --- /dev/null +++ b/client/src/app/business-dashboard/[businessId]/dashboard/containers/dashboard-container.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { DashboardHeader } from "../components/dashboard-header"; +import { StatsGrid } from "../components/stats-grid"; +import { AppointmentsTable } from "../components/appointments-table"; +import AddClientModal from "../../clients/components/add-client-modal"; +import { useState } from "react"; +import { useParams } from "next/navigation"; + +export function DashboardContainer({ + totalClients, + totalBookings, +}: { + totalClients: number; + totalBookings: number; +}) { + const params = useParams(); + const businessId = Number(params.businessId); + const [isAddClientModalOpen, setIsAddClientModalOpen] = useState(false); + return ( +
+ setIsAddClientModalOpen(true)} /> + + + {/* Appointments Table */} + + + {/* Main Content Grid */} + setIsAddClientModalOpen(false)} + businessId={businessId} + /> +
+ ); +} diff --git a/client/src/app/business-dashboard/[businessId]/dashboard/page.tsx b/client/src/app/business-dashboard/[businessId]/dashboard/page.tsx new file mode 100644 index 0000000..9b3769b --- /dev/null +++ b/client/src/app/business-dashboard/[businessId]/dashboard/page.tsx @@ -0,0 +1,30 @@ +import { getTotalClients, getTotalBookings } from "@/lib/business-dashboardAPI"; +import { DashboardContainer } from "./containers/dashboard-container"; + +// Default business ID (can be overridden by dynamic routes) +const DEFAULT_BUSINESS_ID = 3; + +interface PageProps { + params: Promise<{ + businessId: string; + }>; +} + +export default async function DashboardPage({ params }: PageProps) { + const resolvedParams = await params; + const businessId = resolvedParams?.businessId + ? parseInt(resolvedParams.businessId) + : DEFAULT_BUSINESS_ID; + + const [totalClients, totalBookings] = await Promise.all([ + getTotalClients(businessId), + getTotalBookings(businessId), + ]); + + return ( + + ); +} diff --git a/client/src/app/business-dashboard/[businessId]/page.tsx b/client/src/app/business-dashboard/[businessId]/page.tsx new file mode 100644 index 0000000..4543a4c --- /dev/null +++ b/client/src/app/business-dashboard/[businessId]/page.tsx @@ -0,0 +1,74 @@ +"use client"; +import { useParams } from "next/navigation"; +import { useState, useEffect } from "react"; +import { BusinessProvider } from "@/contexts/BusinessContext"; +import { ProtectedRoute } from "@/components/ProtectedRoute"; +import { DashboardContainer } from "./dashboard/containers/dashboard-container"; +import { getTotalClients, getTotalBookings } from "@/lib/business-dashboardAPI"; + +// Component that fetches and displays business dashboard +function BusinessDashboard({ businessId }: { businessId: number }) { + const [stats, setStats] = useState({ totalClients: 0, totalBookings: 0 }); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Use API utilities instead of raw fetch + Promise.all([getTotalClients(businessId), getTotalBookings(businessId)]) + .then(([totalClients, totalBookings]) => { + setStats({ + totalClients, + totalBookings, + }); + setLoading(false); + }) + .catch(() => { + setLoading(false); + }); + }, [businessId]); + + if (loading) { + return ( +
+
+
+
+
+
+
+
+
+
+ ); + } + + return ( + + ); +} + +export default function BusinessDashboardPage() { + const params = useParams(); + const businessId = parseInt(params.businessId as string, 10); + + if (isNaN(businessId)) { + return ( +
+

+ Invalid Business ID +

+

Please check the URL and try again.

+
+ ); + } + + return ( + + + + + + ); +} diff --git a/client/src/app/business-dashboard/[businessId]/services/components/add-service-modal.tsx b/client/src/app/business-dashboard/[businessId]/services/components/add-service-modal.tsx new file mode 100644 index 0000000..29dbb1b --- /dev/null +++ b/client/src/app/business-dashboard/[businessId]/services/components/add-service-modal.tsx @@ -0,0 +1,393 @@ +import React from "react"; +import Image from "next/image"; +import { useAppDispatch } from "@/store/hooks"; +import { addService, editService } from "@/store/services/servicesSlice"; +import type { Service } from "@/store/services/servicesSlice"; + +type AddServiceModalProps = { + open: boolean; + onClose: () => void; + businessId: number; + service?: Service | null; +}; + +export default function AddServiceModal({ + open, + onClose, + businessId, + service = null, +}: AddServiceModalProps) { + const dispatch = useAppDispatch(); + + // form state (strings for controlled inputs) + const [title, setTitle] = React.useState(service?.name || ""); + const [description, setDescription] = React.useState( + service?.description || "" + ); + const [price, setPrice] = React.useState( + service?.price != null ? String(service.price) : "" + ); + const [duration, setDuration] = React.useState( + service?.duration_minutes ? String(service.duration_minutes / 60) : "" + ); + const [status, setStatus] = React.useState(service?.status === "active"); + const [loading, setLoading] = React.useState(false); + const [error, setError] = React.useState(null); + const [validationErrors, setValidationErrors] = React.useState< + Record + >({}); + const [photo, setPhoto] = React.useState(null); + const [photoPreview, setPhotoPreview] = React.useState(""); + + function handleFileChange(file: File) { + setPhoto(file); + setPhotoPreview(URL.createObjectURL(file)); + } + + // Reset form when service changes or modal re-opens + React.useEffect(() => { + setTitle(service?.name || ""); + setDescription(service?.description || ""); + setPrice(service?.price != null ? String(service.price) : ""); + setDuration( + service?.duration_minutes ? String(service.duration_minutes / 60) : "" + ); + setStatus(service?.status === "active"); + setError(null); + setValidationErrors({}); + setPhoto(null); + setPhotoPreview(""); + }, [service, open]); + + // Client-side validation + function validate() { + const errors: Record = {}; + if (!title.trim()) errors.title = "Service title is required."; + if (duration && Number.isNaN(Number(duration))) + errors.duration = "Duration must be a number (hours)."; + if (price && Number.isNaN(Number(price))) + errors.price = "Price must be a number."; + setValidationErrors(errors); + return Object.keys(errors).length === 0; + } + + // API payload + async function fileToBase64(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => resolve(reader.result as string); + reader.onerror = (error) => reject(error); + }); + } + + type ServicePayload = { + name: string; + description?: string; + price: number; + duration_minutes: number; + status: "active" | "inactive"; + photo_base64?: string; + }; + + async function buildPayload(): Promise { + const payload: ServicePayload = { + name: title.trim(), + description: description.trim() || undefined, + price: price === "" ? 0 : Number(price), + duration_minutes: duration === "" ? 0 : Math.round(Number(duration) * 60), + status: status ? "active" : "inactive", + }; + if (photo) { + payload.photo_base64 = await fileToBase64(photo); + } + return payload; + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + if (!validate()) return; + + setLoading(true); + + try { + const payload = await buildPayload(); + + if (service && service.id) { + await dispatch( + editService({ + businessId, + serviceId: service.id, + serviceData: payload, + }) + ); + } else { + await dispatch(addService({ businessId, serviceData: payload })); + } + + setLoading(false); + onClose(); + } catch (err: unknown) { + setLoading(false); + if ( + typeof err === "object" && + err !== null && + "response" in err && + (err as { response?: { data?: { message?: string } } }).response?.data + ) { + setError( + (err as { response?: { data?: { message?: string } } }).response?.data + ?.message || "Failed to save service." + ); + } else { + setError("Failed to save service."); + } + } + } + + if (!open) return null; + + return ( +
+
+ {/* Close button */} + + +

+ {service ? "Edit Service" : "Add New Service"} +

+ + {error && ( +
{error}
+ )} + +
+ {/* Service Name */} +
+ + setTitle(e.target.value)} + required + placeholder="Enter service name" + /> +
+ {/* Description */} +
+ +