Products are nearly finished, also switched to mock api
This commit is contained in:
parent
0986336aea
commit
c60ec969d5
@ -29,7 +29,7 @@
|
|||||||
"@radix-ui/react-scroll-area": "^1.2.1",
|
"@radix-ui/react-scroll-area": "^1.2.1",
|
||||||
"@radix-ui/react-select": "^2.1.2",
|
"@radix-ui/react-select": "^2.1.2",
|
||||||
"@radix-ui/react-separator": "^1.1.0",
|
"@radix-ui/react-separator": "^1.1.0",
|
||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-slot": "^1.2.0",
|
||||||
"@radix-ui/react-switch": "^1.1.1",
|
"@radix-ui/react-switch": "^1.1.1",
|
||||||
"@radix-ui/react-tabs": "^1.1.1",
|
"@radix-ui/react-tabs": "^1.1.1",
|
||||||
"@radix-ui/react-toast": "^1.2.2",
|
"@radix-ui/react-toast": "^1.2.2",
|
||||||
@ -45,6 +45,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.4",
|
"cmdk": "^1.0.4",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
|
"dexie": "^4.0.11",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"lucide-react": "^0.475.0",
|
"lucide-react": "^0.475.0",
|
||||||
@ -56,6 +57,7 @@
|
|||||||
"recharts": "^2.14.1",
|
"recharts": "^2.14.1",
|
||||||
"tailwind-merge": "^3.0.1",
|
"tailwind-merge": "^3.0.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"uuid": "^11.1.0",
|
||||||
"zod": "^3.24.2",
|
"zod": "^3.24.2",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
@ -86,6 +88,7 @@
|
|||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "~5.7.2",
|
"typescript": "~5.7.2",
|
||||||
"typescript-eslint": "^8.22.0",
|
"typescript-eslint": "^8.22.0",
|
||||||
"vite": "^6.1.0"
|
"vite": "^6.1.0",
|
||||||
|
"vite-plugin-svgr": "^4.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
298
frontend/pnpm-lock.yaml
generated
298
frontend/pnpm-lock.yaml
generated
@ -54,7 +54,7 @@ importers:
|
|||||||
specifier: ^1.1.0
|
specifier: ^1.1.0
|
||||||
version: 1.1.3(@types/react-dom@19.1.2(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 1.1.3(@types/react-dom@19.1.2(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-slot':
|
'@radix-ui/react-slot':
|
||||||
specifier: ^1.1.2
|
specifier: ^1.2.0
|
||||||
version: 1.2.0(@types/react@19.1.0)(react@19.1.0)
|
version: 1.2.0(@types/react@19.1.0)(react@19.1.0)
|
||||||
'@radix-ui/react-switch':
|
'@radix-ui/react-switch':
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
@ -101,6 +101,9 @@ importers:
|
|||||||
date-fns:
|
date-fns:
|
||||||
specifier: ^3.6.0
|
specifier: ^3.6.0
|
||||||
version: 3.6.0
|
version: 3.6.0
|
||||||
|
dexie:
|
||||||
|
specifier: ^4.0.11
|
||||||
|
version: 4.0.11
|
||||||
js-cookie:
|
js-cookie:
|
||||||
specifier: ^3.0.5
|
specifier: ^3.0.5
|
||||||
version: 3.0.5
|
version: 3.0.5
|
||||||
@ -134,6 +137,9 @@ importers:
|
|||||||
tailwindcss-animate:
|
tailwindcss-animate:
|
||||||
specifier: ^1.0.7
|
specifier: ^1.0.7
|
||||||
version: 1.0.7(tailwindcss@3.4.17)
|
version: 1.0.7(tailwindcss@3.4.17)
|
||||||
|
uuid:
|
||||||
|
specifier: ^11.1.0
|
||||||
|
version: 11.1.0
|
||||||
zod:
|
zod:
|
||||||
specifier: ^3.24.2
|
specifier: ^3.24.2
|
||||||
version: 3.24.2
|
version: 3.24.2
|
||||||
@ -222,6 +228,9 @@ importers:
|
|||||||
vite:
|
vite:
|
||||||
specifier: ^6.1.0
|
specifier: ^6.1.0
|
||||||
version: 6.2.6(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.1)
|
version: 6.2.6(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.1)
|
||||||
|
vite-plugin-svgr:
|
||||||
|
specifier: ^4.3.0
|
||||||
|
version: 4.3.0(rollup@4.39.0)(typescript@5.7.3)(vite@6.2.6(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.1))
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@ -1147,6 +1156,15 @@ packages:
|
|||||||
'@radix-ui/rect@1.1.1':
|
'@radix-ui/rect@1.1.1':
|
||||||
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
||||||
|
|
||||||
|
'@rollup/pluginutils@5.1.4':
|
||||||
|
resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
rollup:
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@rollup/rollup-android-arm-eabi@4.39.0':
|
'@rollup/rollup-android-arm-eabi@4.39.0':
|
||||||
resolution: {integrity: sha512-lGVys55Qb00Wvh8DMAocp5kIcaNzEFTmGhfFd88LfaogYTRKrdxgtlO5H6S49v2Nd8R2C6wLOal0qv6/kCkOwA==}
|
resolution: {integrity: sha512-lGVys55Qb00Wvh8DMAocp5kIcaNzEFTmGhfFd88LfaogYTRKrdxgtlO5H6S49v2Nd8R2C6wLOal0qv6/kCkOwA==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
@ -1247,6 +1265,74 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@svgr/babel-plugin-add-jsx-attribute@8.0.0':
|
||||||
|
resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
peerDependencies:
|
||||||
|
'@babel/core': ^7.0.0-0
|
||||||
|
|
||||||
|
'@svgr/babel-plugin-remove-jsx-attribute@8.0.0':
|
||||||
|
resolution: {integrity: sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
peerDependencies:
|
||||||
|
'@babel/core': ^7.0.0-0
|
||||||
|
|
||||||
|
'@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0':
|
||||||
|
resolution: {integrity: sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
peerDependencies:
|
||||||
|
'@babel/core': ^7.0.0-0
|
||||||
|
|
||||||
|
'@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0':
|
||||||
|
resolution: {integrity: sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
peerDependencies:
|
||||||
|
'@babel/core': ^7.0.0-0
|
||||||
|
|
||||||
|
'@svgr/babel-plugin-svg-dynamic-title@8.0.0':
|
||||||
|
resolution: {integrity: sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
peerDependencies:
|
||||||
|
'@babel/core': ^7.0.0-0
|
||||||
|
|
||||||
|
'@svgr/babel-plugin-svg-em-dimensions@8.0.0':
|
||||||
|
resolution: {integrity: sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
peerDependencies:
|
||||||
|
'@babel/core': ^7.0.0-0
|
||||||
|
|
||||||
|
'@svgr/babel-plugin-transform-react-native-svg@8.1.0':
|
||||||
|
resolution: {integrity: sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
peerDependencies:
|
||||||
|
'@babel/core': ^7.0.0-0
|
||||||
|
|
||||||
|
'@svgr/babel-plugin-transform-svg-component@8.0.0':
|
||||||
|
resolution: {integrity: sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
peerDependencies:
|
||||||
|
'@babel/core': ^7.0.0-0
|
||||||
|
|
||||||
|
'@svgr/babel-preset@8.1.0':
|
||||||
|
resolution: {integrity: sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
peerDependencies:
|
||||||
|
'@babel/core': ^7.0.0-0
|
||||||
|
|
||||||
|
'@svgr/core@8.1.0':
|
||||||
|
resolution: {integrity: sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
'@svgr/hast-util-to-babel-ast@8.0.0':
|
||||||
|
resolution: {integrity: sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
'@svgr/plugin-jsx@8.1.0':
|
||||||
|
resolution: {integrity: sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
peerDependencies:
|
||||||
|
'@svgr/core': '*'
|
||||||
|
|
||||||
'@swc/core-darwin-arm64@1.11.18':
|
'@swc/core-darwin-arm64@1.11.18':
|
||||||
resolution: {integrity: sha512-K6AntdUlNMQg8aChqjeXwnVhK6d4WRZ9TgtLSTmdU0Ugll4an7QK49s9NrT7XQU91cEsVvzdr++p1bNImx0hJg==}
|
resolution: {integrity: sha512-K6AntdUlNMQg8aChqjeXwnVhK6d4WRZ9TgtLSTmdU0Ugll4an7QK49s9NrT7XQU91cEsVvzdr++p1bNImx0hJg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -1691,6 +1777,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
|
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
camelcase@6.3.0:
|
||||||
|
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
caniuse-lite@1.0.30001713:
|
caniuse-lite@1.0.30001713:
|
||||||
resolution: {integrity: sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==}
|
resolution: {integrity: sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==}
|
||||||
|
|
||||||
@ -1765,6 +1855,15 @@ packages:
|
|||||||
convert-source-map@2.0.0:
|
convert-source-map@2.0.0:
|
||||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
|
|
||||||
|
cosmiconfig@8.3.6:
|
||||||
|
resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=4.9.5'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
|
||||||
country-flag-icons@1.5.18:
|
country-flag-icons@1.5.18:
|
||||||
resolution: {integrity: sha512-z+Uzesi8u8IdkViqqbzzbkf3+a7WJpcET5B7sPwTg7GXqPYpVEgNlZ/FC3l8KO4mEf+mNkmzKLppKTN4PlCJEQ==}
|
resolution: {integrity: sha512-z+Uzesi8u8IdkViqqbzzbkf3+a7WJpcET5B7sPwTg7GXqPYpVEgNlZ/FC3l8KO4mEf+mNkmzKLppKTN4PlCJEQ==}
|
||||||
|
|
||||||
@ -1864,6 +1963,9 @@ packages:
|
|||||||
detect-node-es@1.1.0:
|
detect-node-es@1.1.0:
|
||||||
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
|
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
|
||||||
|
|
||||||
|
dexie@4.0.11:
|
||||||
|
resolution: {integrity: sha512-SOKO002EqlvBYYKQSew3iymBoN2EQ4BDw/3yprjh7kAfFzjBYkaMNa/pZvcA7HSWlcKSQb9XhPe3wKyQ0x4A8A==}
|
||||||
|
|
||||||
didyoumean@1.2.2:
|
didyoumean@1.2.2:
|
||||||
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
||||||
|
|
||||||
@ -1877,6 +1979,9 @@ packages:
|
|||||||
dom-helpers@5.2.1:
|
dom-helpers@5.2.1:
|
||||||
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
|
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
|
||||||
|
|
||||||
|
dot-case@3.0.4:
|
||||||
|
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
|
||||||
|
|
||||||
dotenv@16.4.7:
|
dotenv@16.4.7:
|
||||||
resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
|
resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -1904,6 +2009,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
|
resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
|
entities@4.5.0:
|
||||||
|
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||||
|
engines: {node: '>=0.12'}
|
||||||
|
|
||||||
|
error-ex@1.3.2:
|
||||||
|
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
|
||||||
|
|
||||||
es-define-property@1.0.1:
|
es-define-property@1.0.1:
|
||||||
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -1982,6 +2094,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
|
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
|
|
||||||
|
estree-walker@2.0.2:
|
||||||
|
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||||
|
|
||||||
esutils@2.0.3:
|
esutils@2.0.3:
|
||||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@ -2170,6 +2285,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
|
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
is-arrayish@0.2.1:
|
||||||
|
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
|
||||||
|
|
||||||
is-binary-path@2.1.0:
|
is-binary-path@2.1.0:
|
||||||
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -2235,6 +2353,9 @@ packages:
|
|||||||
json-buffer@3.0.1:
|
json-buffer@3.0.1:
|
||||||
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
|
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
|
||||||
|
|
||||||
|
json-parse-even-better-errors@2.3.1:
|
||||||
|
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
|
||||||
|
|
||||||
json-schema-traverse@0.4.1:
|
json-schema-traverse@0.4.1:
|
||||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||||
|
|
||||||
@ -2289,6 +2410,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
lower-case@2.0.2:
|
||||||
|
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
|
||||||
|
|
||||||
lru-cache@10.4.3:
|
lru-cache@10.4.3:
|
||||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||||
|
|
||||||
@ -2371,6 +2495,9 @@ packages:
|
|||||||
neo-async@2.6.2:
|
neo-async@2.6.2:
|
||||||
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
||||||
|
|
||||||
|
no-case@3.0.4:
|
||||||
|
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
|
||||||
|
|
||||||
node-fetch-native@1.6.6:
|
node-fetch-native@1.6.6:
|
||||||
resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==}
|
resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==}
|
||||||
|
|
||||||
@ -2420,6 +2547,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
parse-json@5.2.0:
|
||||||
|
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
parse-ms@4.0.0:
|
parse-ms@4.0.0:
|
||||||
resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==}
|
resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@ -2439,6 +2570,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
||||||
engines: {node: '>=16 || 14 >=14.18'}
|
engines: {node: '>=16 || 14 >=14.18'}
|
||||||
|
|
||||||
|
path-type@4.0.0:
|
||||||
|
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
pathe@1.1.2:
|
pathe@1.1.2:
|
||||||
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
|
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
|
||||||
|
|
||||||
@ -2760,6 +2895,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ==}
|
resolution: {integrity: sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
|
snake-case@3.0.4:
|
||||||
|
resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==}
|
||||||
|
|
||||||
solid-js@1.9.5:
|
solid-js@1.9.5:
|
||||||
resolution: {integrity: sha512-ogI3DaFcyn6UhYhrgcyRAMbu/buBJitYQASZz5WzfQVPP10RD2AbCoRZ517psnezrasyCbWzIxZ6kVqet768xw==}
|
resolution: {integrity: sha512-ogI3DaFcyn6UhYhrgcyRAMbu/buBJitYQASZz5WzfQVPP10RD2AbCoRZ517psnezrasyCbWzIxZ6kVqet768xw==}
|
||||||
|
|
||||||
@ -2812,6 +2950,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
svg-parser@2.0.4:
|
||||||
|
resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==}
|
||||||
|
|
||||||
tailwind-merge@3.2.0:
|
tailwind-merge@3.2.0:
|
||||||
resolution: {integrity: sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==}
|
resolution: {integrity: sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==}
|
||||||
|
|
||||||
@ -2942,9 +3083,18 @@ packages:
|
|||||||
util-deprecate@1.0.2:
|
util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
|
uuid@11.1.0:
|
||||||
|
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
victory-vendor@36.9.2:
|
victory-vendor@36.9.2:
|
||||||
resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
|
resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
|
||||||
|
|
||||||
|
vite-plugin-svgr@4.3.0:
|
||||||
|
resolution: {integrity: sha512-Jy9qLB2/PyWklpYy0xk0UU3TlU0t2UMpJXZvf+hWII1lAmRHrOUKi11Uw8N3rxoNk7atZNYO3pR3vI1f7oi+6w==}
|
||||||
|
peerDependencies:
|
||||||
|
vite: '>=2.6.0'
|
||||||
|
|
||||||
vite@6.2.6:
|
vite@6.2.6:
|
||||||
resolution: {integrity: sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==}
|
resolution: {integrity: sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==}
|
||||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||||
@ -3979,6 +4129,14 @@ snapshots:
|
|||||||
|
|
||||||
'@radix-ui/rect@1.1.1': {}
|
'@radix-ui/rect@1.1.1': {}
|
||||||
|
|
||||||
|
'@rollup/pluginutils@5.1.4(rollup@4.39.0)':
|
||||||
|
dependencies:
|
||||||
|
'@types/estree': 1.0.7
|
||||||
|
estree-walker: 2.0.2
|
||||||
|
picomatch: 4.0.2
|
||||||
|
optionalDependencies:
|
||||||
|
rollup: 4.39.0
|
||||||
|
|
||||||
'@rollup/rollup-android-arm-eabi@4.39.0':
|
'@rollup/rollup-android-arm-eabi@4.39.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -4039,6 +4197,76 @@ snapshots:
|
|||||||
'@rollup/rollup-win32-x64-msvc@4.39.0':
|
'@rollup/rollup-win32-x64-msvc@4.39.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.26.10)':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.26.10
|
||||||
|
|
||||||
|
'@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.26.10)':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.26.10
|
||||||
|
|
||||||
|
'@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.26.10)':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.26.10
|
||||||
|
|
||||||
|
'@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.26.10)':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.26.10
|
||||||
|
|
||||||
|
'@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.26.10)':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.26.10
|
||||||
|
|
||||||
|
'@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.26.10)':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.26.10
|
||||||
|
|
||||||
|
'@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.26.10)':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.26.10
|
||||||
|
|
||||||
|
'@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.26.10)':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.26.10
|
||||||
|
|
||||||
|
'@svgr/babel-preset@8.1.0(@babel/core@7.26.10)':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.26.10
|
||||||
|
'@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.26.10)
|
||||||
|
'@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.26.10)
|
||||||
|
'@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.26.10)
|
||||||
|
'@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.26.10)
|
||||||
|
'@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.26.10)
|
||||||
|
'@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.26.10)
|
||||||
|
'@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.26.10)
|
||||||
|
'@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.26.10)
|
||||||
|
|
||||||
|
'@svgr/core@8.1.0(typescript@5.7.3)':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.26.10
|
||||||
|
'@svgr/babel-preset': 8.1.0(@babel/core@7.26.10)
|
||||||
|
camelcase: 6.3.0
|
||||||
|
cosmiconfig: 8.3.6(typescript@5.7.3)
|
||||||
|
snake-case: 3.0.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
- typescript
|
||||||
|
|
||||||
|
'@svgr/hast-util-to-babel-ast@8.0.0':
|
||||||
|
dependencies:
|
||||||
|
'@babel/types': 7.27.0
|
||||||
|
entities: 4.5.0
|
||||||
|
|
||||||
|
'@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.7.3))':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.26.10
|
||||||
|
'@svgr/babel-preset': 8.1.0(@babel/core@7.26.10)
|
||||||
|
'@svgr/core': 8.1.0(typescript@5.7.3)
|
||||||
|
'@svgr/hast-util-to-babel-ast': 8.0.0
|
||||||
|
svg-parser: 2.0.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
'@swc/core-darwin-arm64@1.11.18':
|
'@swc/core-darwin-arm64@1.11.18':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -4523,6 +4751,8 @@ snapshots:
|
|||||||
|
|
||||||
camelcase-css@2.0.1: {}
|
camelcase-css@2.0.1: {}
|
||||||
|
|
||||||
|
camelcase@6.3.0: {}
|
||||||
|
|
||||||
caniuse-lite@1.0.30001713: {}
|
caniuse-lite@1.0.30001713: {}
|
||||||
|
|
||||||
chalk@4.1.2:
|
chalk@4.1.2:
|
||||||
@ -4597,6 +4827,15 @@ snapshots:
|
|||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
convert-source-map@2.0.0: {}
|
||||||
|
|
||||||
|
cosmiconfig@8.3.6(typescript@5.7.3):
|
||||||
|
dependencies:
|
||||||
|
import-fresh: 3.3.1
|
||||||
|
js-yaml: 4.1.0
|
||||||
|
parse-json: 5.2.0
|
||||||
|
path-type: 4.0.0
|
||||||
|
optionalDependencies:
|
||||||
|
typescript: 5.7.3
|
||||||
|
|
||||||
country-flag-icons@1.5.18: {}
|
country-flag-icons@1.5.18: {}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
@ -4674,6 +4913,8 @@ snapshots:
|
|||||||
|
|
||||||
detect-node-es@1.1.0: {}
|
detect-node-es@1.1.0: {}
|
||||||
|
|
||||||
|
dexie@4.0.11: {}
|
||||||
|
|
||||||
didyoumean@1.2.2: {}
|
didyoumean@1.2.2: {}
|
||||||
|
|
||||||
diff@7.0.0: {}
|
diff@7.0.0: {}
|
||||||
@ -4685,6 +4926,11 @@ snapshots:
|
|||||||
'@babel/runtime': 7.27.0
|
'@babel/runtime': 7.27.0
|
||||||
csstype: 3.1.3
|
csstype: 3.1.3
|
||||||
|
|
||||||
|
dot-case@3.0.4:
|
||||||
|
dependencies:
|
||||||
|
no-case: 3.0.4
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
dotenv@16.4.7: {}
|
dotenv@16.4.7: {}
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
@ -4712,6 +4958,12 @@ snapshots:
|
|||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
tapable: 2.2.1
|
tapable: 2.2.1
|
||||||
|
|
||||||
|
entities@4.5.0: {}
|
||||||
|
|
||||||
|
error-ex@1.3.2:
|
||||||
|
dependencies:
|
||||||
|
is-arrayish: 0.2.1
|
||||||
|
|
||||||
es-define-property@1.0.1: {}
|
es-define-property@1.0.1: {}
|
||||||
|
|
||||||
es-errors@1.3.0: {}
|
es-errors@1.3.0: {}
|
||||||
@ -4834,6 +5086,8 @@ snapshots:
|
|||||||
|
|
||||||
estraverse@5.3.0: {}
|
estraverse@5.3.0: {}
|
||||||
|
|
||||||
|
estree-walker@2.0.2: {}
|
||||||
|
|
||||||
esutils@2.0.3: {}
|
esutils@2.0.3: {}
|
||||||
|
|
||||||
eventemitter3@4.0.7: {}
|
eventemitter3@4.0.7: {}
|
||||||
@ -5011,6 +5265,8 @@ snapshots:
|
|||||||
|
|
||||||
internmap@2.0.3: {}
|
internmap@2.0.3: {}
|
||||||
|
|
||||||
|
is-arrayish@0.2.1: {}
|
||||||
|
|
||||||
is-binary-path@2.1.0:
|
is-binary-path@2.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
binary-extensions: 2.3.0
|
binary-extensions: 2.3.0
|
||||||
@ -5057,6 +5313,8 @@ snapshots:
|
|||||||
|
|
||||||
json-buffer@3.0.1: {}
|
json-buffer@3.0.1: {}
|
||||||
|
|
||||||
|
json-parse-even-better-errors@2.3.1: {}
|
||||||
|
|
||||||
json-schema-traverse@0.4.1: {}
|
json-schema-traverse@0.4.1: {}
|
||||||
|
|
||||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||||
@ -5111,6 +5369,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
js-tokens: 4.0.0
|
js-tokens: 4.0.0
|
||||||
|
|
||||||
|
lower-case@2.0.2:
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
lru-cache@10.4.3: {}
|
lru-cache@10.4.3: {}
|
||||||
|
|
||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
@ -5182,6 +5444,11 @@ snapshots:
|
|||||||
|
|
||||||
neo-async@2.6.2: {}
|
neo-async@2.6.2: {}
|
||||||
|
|
||||||
|
no-case@3.0.4:
|
||||||
|
dependencies:
|
||||||
|
lower-case: 2.0.2
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
node-fetch-native@1.6.6: {}
|
node-fetch-native@1.6.6: {}
|
||||||
|
|
||||||
node-releases@2.0.19: {}
|
node-releases@2.0.19: {}
|
||||||
@ -5228,6 +5495,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
callsites: 3.1.0
|
callsites: 3.1.0
|
||||||
|
|
||||||
|
parse-json@5.2.0:
|
||||||
|
dependencies:
|
||||||
|
'@babel/code-frame': 7.26.2
|
||||||
|
error-ex: 1.3.2
|
||||||
|
json-parse-even-better-errors: 2.3.1
|
||||||
|
lines-and-columns: 1.2.4
|
||||||
|
|
||||||
parse-ms@4.0.0: {}
|
parse-ms@4.0.0: {}
|
||||||
|
|
||||||
path-exists@4.0.0: {}
|
path-exists@4.0.0: {}
|
||||||
@ -5241,6 +5515,8 @@ snapshots:
|
|||||||
lru-cache: 10.4.3
|
lru-cache: 10.4.3
|
||||||
minipass: 7.1.2
|
minipass: 7.1.2
|
||||||
|
|
||||||
|
path-type@4.0.0: {}
|
||||||
|
|
||||||
pathe@1.1.2: {}
|
pathe@1.1.2: {}
|
||||||
|
|
||||||
pathe@2.0.3: {}
|
pathe@2.0.3: {}
|
||||||
@ -5502,6 +5778,11 @@ snapshots:
|
|||||||
|
|
||||||
smol-toml@1.3.1: {}
|
smol-toml@1.3.1: {}
|
||||||
|
|
||||||
|
snake-case@3.0.4:
|
||||||
|
dependencies:
|
||||||
|
dot-case: 3.0.4
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
solid-js@1.9.5:
|
solid-js@1.9.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
csstype: 3.1.3
|
csstype: 3.1.3
|
||||||
@ -5554,6 +5835,8 @@ snapshots:
|
|||||||
|
|
||||||
supports-preserve-symlinks-flag@1.0.0: {}
|
supports-preserve-symlinks-flag@1.0.0: {}
|
||||||
|
|
||||||
|
svg-parser@2.0.4: {}
|
||||||
|
|
||||||
tailwind-merge@3.2.0: {}
|
tailwind-merge@3.2.0: {}
|
||||||
|
|
||||||
tailwindcss-animate@1.0.7(tailwindcss@3.4.17):
|
tailwindcss-animate@1.0.7(tailwindcss@3.4.17):
|
||||||
@ -5693,6 +5976,8 @@ snapshots:
|
|||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
|
uuid@11.1.0: {}
|
||||||
|
|
||||||
victory-vendor@36.9.2:
|
victory-vendor@36.9.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/d3-array': 3.2.1
|
'@types/d3-array': 3.2.1
|
||||||
@ -5710,6 +5995,17 @@ snapshots:
|
|||||||
d3-time: 3.1.0
|
d3-time: 3.1.0
|
||||||
d3-timer: 3.0.1
|
d3-timer: 3.0.1
|
||||||
|
|
||||||
|
vite-plugin-svgr@4.3.0(rollup@4.39.0)(typescript@5.7.3)(vite@6.2.6(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.1)):
|
||||||
|
dependencies:
|
||||||
|
'@rollup/pluginutils': 5.1.4(rollup@4.39.0)
|
||||||
|
'@svgr/core': 8.1.0(typescript@5.7.3)
|
||||||
|
'@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.7.3))
|
||||||
|
vite: 6.2.6(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.1)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- rollup
|
||||||
|
- supports-color
|
||||||
|
- typescript
|
||||||
|
|
||||||
vite@6.2.6(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.1):
|
vite@6.2.6(@types/node@22.14.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.25.2
|
esbuild: 0.25.2
|
||||||
|
19
frontend/src/api/api-definition.ts
Normal file
19
frontend/src/api/api-definition.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import {
|
||||||
|
UserPublic,
|
||||||
|
UserRegister,
|
||||||
|
UserUpdate,
|
||||||
|
ShopLoginAccessTokenData
|
||||||
|
} from "@/client";
|
||||||
|
import { Shop } from "./mock/models";
|
||||||
|
|
||||||
|
export interface AuthAPI {
|
||||||
|
getCurrentUser(): Promise<UserPublic | null>;
|
||||||
|
registerUser(data: UserRegister): Promise<void>;
|
||||||
|
loginUser(data: ShopLoginAccessTokenData): Promise<{ access_token: string }>;
|
||||||
|
updateUser(data: UserUpdate): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShopAPI {
|
||||||
|
getShop(): Promise<Shop | null>;
|
||||||
|
updateShop(data: Partial<Shop>): Promise<void>;
|
||||||
|
}
|
12
frontend/src/api/api.ts
Normal file
12
frontend/src/api/api.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { RealAuthAPI } from "./real/auth-real-api";
|
||||||
|
import { MockAuthAPI } from "./mock/auth-mock-api";
|
||||||
|
import { AuthAPI, ShopAPI } from "./api-definition";
|
||||||
|
import { MockShopAPI } from "./mock/shop-mock-api";
|
||||||
|
import { MockProductAPI } from "./mock/products-mock-api";
|
||||||
|
|
||||||
|
export const authAPI: AuthAPI =
|
||||||
|
import.meta.env.VITE_USE_MOCK_API === "true" ? MockAuthAPI : RealAuthAPI;
|
||||||
|
|
||||||
|
export const shopAPI: ShopAPI = MockShopAPI;
|
||||||
|
|
||||||
|
export const productsAPI = MockProductAPI;
|
121
frontend/src/api/mock/auth-mock-api.ts
Normal file
121
frontend/src/api/mock/auth-mock-api.ts
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import { AuthAPI } from "../api-definition";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { UserRegister, UserUpdate, ShopLoginAccessTokenData } from "@/client";
|
||||||
|
import { extractSubFromJWT, generateFakeJWT } from "@/utils/jwt";
|
||||||
|
import { mockDB } from "./db";
|
||||||
|
import { MockUser, Shop } from "./models";
|
||||||
|
|
||||||
|
|
||||||
|
const db = mockDB;
|
||||||
|
|
||||||
|
export const MockAuthAPI: AuthAPI = {
|
||||||
|
async getCurrentUser() {
|
||||||
|
const token = localStorage.getItem("access_token");
|
||||||
|
if (!token) return null;
|
||||||
|
|
||||||
|
const userUUID = extractSubFromJWT(token);
|
||||||
|
if (!userUUID) return null;
|
||||||
|
|
||||||
|
const user = await db.users.where("uuid").equals(userUUID).first();
|
||||||
|
if (!user) return null;
|
||||||
|
|
||||||
|
const publicUser = {
|
||||||
|
...user,
|
||||||
|
first_name: user.first_name ?? null,
|
||||||
|
last_name: user.last_name ?? null,
|
||||||
|
profile_picture: user.profile_picture ?? null
|
||||||
|
};
|
||||||
|
return publicUser;
|
||||||
|
},
|
||||||
|
|
||||||
|
async registerUser(data: UserRegister) {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const userId = Date.now();
|
||||||
|
const userUUID = uuidv4();
|
||||||
|
const shopUUID = uuidv4();
|
||||||
|
|
||||||
|
const newUser: MockUser = {
|
||||||
|
id: userId,
|
||||||
|
uuid: userUUID,
|
||||||
|
user_role: "customer",
|
||||||
|
status: "customer",
|
||||||
|
shop_id: null,
|
||||||
|
username: data.username,
|
||||||
|
email: data.email,
|
||||||
|
password: data.password,
|
||||||
|
first_name: undefined,
|
||||||
|
last_name: undefined,
|
||||||
|
phone_number: data.phone_number,
|
||||||
|
profile_picture: undefined,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
last_login: now
|
||||||
|
};
|
||||||
|
|
||||||
|
const newShop: Shop = {
|
||||||
|
id: userId,
|
||||||
|
uuid: shopUUID,
|
||||||
|
owner_id: userId,
|
||||||
|
name: `${data.username}'s Shop`,
|
||||||
|
description: "A newly created shop.",
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
status: "inactive",
|
||||||
|
logo: null,
|
||||||
|
contact_email: data.email,
|
||||||
|
contact_phone_number: data.phone_number,
|
||||||
|
currency: "USD",
|
||||||
|
address: {
|
||||||
|
street: "",
|
||||||
|
city: "",
|
||||||
|
state: null,
|
||||||
|
postal_code: "",
|
||||||
|
country: ""
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await mockDB.transaction('rw', mockDB.users, mockDB.shops, mockDB.preferences, mockDB.statistics, async () => {
|
||||||
|
await mockDB.users.add(newUser);
|
||||||
|
await mockDB.preferences.add({ user_id: userId });
|
||||||
|
await mockDB.statistics.add({ user_id: userId, total_spend: 0 });
|
||||||
|
await mockDB.shops.add(newShop);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async loginUser(data: ShopLoginAccessTokenData) {
|
||||||
|
const user = await db.users
|
||||||
|
.where("email")
|
||||||
|
.equals(data.formData.username)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!user || user.password != data.formData.password) {
|
||||||
|
throw new Error("Invalid credentials");
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.users.update(user.id, { last_login: new Date().toISOString() });
|
||||||
|
|
||||||
|
const token = generateFakeJWT(user.uuid);
|
||||||
|
localStorage.setItem("access_token", token);
|
||||||
|
return { access_token: token };
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateUser(data: UserUpdate) {
|
||||||
|
const token = localStorage.getItem("access_token");
|
||||||
|
if (!token) return;
|
||||||
|
|
||||||
|
const userUUID = extractSubFromJWT(token);
|
||||||
|
if (!userUUID) return;
|
||||||
|
|
||||||
|
await db.users
|
||||||
|
.where("uuid")
|
||||||
|
.equals(userUUID)
|
||||||
|
.modify({
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
email: data.email ?? "",
|
||||||
|
phone_number: data.phone_number ?? "",
|
||||||
|
username: data.username ?? "",
|
||||||
|
first_name: data.first_name ?? undefined,
|
||||||
|
last_name: data.last_name ?? undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
41
frontend/src/api/mock/db.ts
Normal file
41
frontend/src/api/mock/db.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import Dexie, { Table } from "dexie";
|
||||||
|
import {
|
||||||
|
MockUser,
|
||||||
|
MockUserPreferences,
|
||||||
|
MockUserStatistics,
|
||||||
|
Product,
|
||||||
|
ProductCategory,
|
||||||
|
ProductCategoryJunction,
|
||||||
|
ProductImage,
|
||||||
|
ProductVariant,
|
||||||
|
Shop
|
||||||
|
} from "./models";
|
||||||
|
|
||||||
|
class MockDB extends Dexie {
|
||||||
|
users!: Table<MockUser, number>;
|
||||||
|
preferences!: Table<MockUserPreferences, number>;
|
||||||
|
statistics!: Table<MockUserStatistics, number>;
|
||||||
|
shops!: Table<Shop, number>;
|
||||||
|
products!: Table<Product, number>;
|
||||||
|
product_variants!: Table<ProductVariant, number>;
|
||||||
|
product_images!: Table<ProductImage, number>;
|
||||||
|
product_categories!: Table<ProductCategory, number>;
|
||||||
|
product_category_junctions!: Table<ProductCategoryJunction, [number, number]>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super("MockDB");
|
||||||
|
this.version(1).stores({
|
||||||
|
users: "++id,username,email,uuid",
|
||||||
|
preferences: "user_id",
|
||||||
|
statistics: "user_id",
|
||||||
|
shops: "++id,uuid,name",
|
||||||
|
products: "++id,shop_id,name",
|
||||||
|
product_variants: "++id,product_id",
|
||||||
|
product_images: "++id,product_id",
|
||||||
|
product_categories: "++id,parent_category_id",
|
||||||
|
product_category_junctions: "[product_id+category_id]"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mockDB = new MockDB();
|
107
frontend/src/api/mock/models.ts
Normal file
107
frontend/src/api/mock/models.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { UserRegister } from "@/client";
|
||||||
|
|
||||||
|
export type UserRole = "owner" | "customer" | "employee" | "manager" | "admin";
|
||||||
|
|
||||||
|
export interface MockUser extends Omit<UserRegister, "password"> {
|
||||||
|
id: number;
|
||||||
|
uuid: string;
|
||||||
|
user_role: UserRole;
|
||||||
|
shop_id: number | null;
|
||||||
|
status: UserRole;
|
||||||
|
password: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
last_login?: string;
|
||||||
|
first_name?: string;
|
||||||
|
last_name?: string;
|
||||||
|
profile_picture?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MockUserPreferences {
|
||||||
|
user_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MockUserStatistics {
|
||||||
|
user_id: number;
|
||||||
|
total_spend?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ShopStatus = "active" | "inactive" | "suspended";
|
||||||
|
|
||||||
|
export interface ShopAddress {
|
||||||
|
street: string;
|
||||||
|
city: string;
|
||||||
|
state?: string | null;
|
||||||
|
postal_code: string;
|
||||||
|
country: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Shop {
|
||||||
|
id: number;
|
||||||
|
uuid: string;
|
||||||
|
owner_id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
status: ShopStatus;
|
||||||
|
logo?: string | null;
|
||||||
|
contact_email: string;
|
||||||
|
contact_phone_number: string;
|
||||||
|
address: ShopAddress;
|
||||||
|
currency: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Product {
|
||||||
|
id: number;
|
||||||
|
shop_id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
price: number;
|
||||||
|
stock_quantity: number;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
export interface ProductCategoryJunction {
|
||||||
|
product_id: number;
|
||||||
|
category_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductCategory {
|
||||||
|
id: number;
|
||||||
|
parent_category_id?: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductImage {
|
||||||
|
id: number;
|
||||||
|
product_id: number;
|
||||||
|
image_id: number;
|
||||||
|
image_url: string;
|
||||||
|
alt_text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductVariant {
|
||||||
|
id: number;
|
||||||
|
product_id: number;
|
||||||
|
index: number; // used for ordering
|
||||||
|
name: string;
|
||||||
|
price: number;
|
||||||
|
comment?: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductCreate {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
price: number;
|
||||||
|
stock_quantity: number;
|
||||||
|
image_data?: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductWithDetails extends Product {
|
||||||
|
images: ProductImage[];
|
||||||
|
variants: ProductVariant[];
|
||||||
|
categories: ProductCategory[];
|
||||||
|
}
|
163
frontend/src/api/mock/products-mock-api.ts
Normal file
163
frontend/src/api/mock/products-mock-api.ts
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
import { mockDB } from "./db";
|
||||||
|
import { ProductCategory, ProductCreate, ProductWithDetails } from "./models";
|
||||||
|
import { getCurrentUserDirect } from "./utils/currentUser";
|
||||||
|
|
||||||
|
export const MockProductAPI = {
|
||||||
|
async getProductsForShop(): Promise<ProductWithDetails[]> {
|
||||||
|
const user = await getCurrentUserDirect();
|
||||||
|
if (!user?.id) return [];
|
||||||
|
|
||||||
|
const products = await mockDB.products
|
||||||
|
.where("shop_id")
|
||||||
|
.equals(user.id)
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
const detailedProducts = await Promise.all(
|
||||||
|
products.map(async (product) => {
|
||||||
|
const images = await mockDB.product_images
|
||||||
|
.where("product_id")
|
||||||
|
.equals(product.id)
|
||||||
|
.toArray();
|
||||||
|
const variants = await mockDB.product_variants
|
||||||
|
.where("product_id")
|
||||||
|
.equals(product.id)
|
||||||
|
.toArray();
|
||||||
|
const categoryLinks = await mockDB.product_category_junctions
|
||||||
|
.where("product_id")
|
||||||
|
.equals(product.id)
|
||||||
|
.toArray();
|
||||||
|
const rawCategories = await Promise.all(
|
||||||
|
categoryLinks.map((link) =>
|
||||||
|
mockDB.product_categories.get(link.category_id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const categories = rawCategories.filter(
|
||||||
|
(c): c is ProductCategory => c !== undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...product,
|
||||||
|
images,
|
||||||
|
variants,
|
||||||
|
categories
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return detailedProducts;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getProductById(productId: number): Promise<ProductWithDetails | null> {
|
||||||
|
const product = await mockDB.products.get(productId);
|
||||||
|
if (!product) return null;
|
||||||
|
|
||||||
|
const images = await mockDB.product_images
|
||||||
|
.where("product_id")
|
||||||
|
.equals(productId)
|
||||||
|
.toArray();
|
||||||
|
const variants = await mockDB.product_variants
|
||||||
|
.where("product_id")
|
||||||
|
.equals(productId)
|
||||||
|
.toArray();
|
||||||
|
const categoryLinks = await mockDB.product_category_junctions
|
||||||
|
.where("product_id")
|
||||||
|
.equals(productId)
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
const rawCategories = await Promise.all(
|
||||||
|
categoryLinks.map((link) =>
|
||||||
|
mockDB.product_categories.get(link.category_id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const categories = rawCategories.filter(
|
||||||
|
(c): c is ProductCategory => c !== undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...product,
|
||||||
|
images,
|
||||||
|
variants,
|
||||||
|
categories
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async createProduct(productData: ProductCreate) {
|
||||||
|
const user = await getCurrentUserDirect();
|
||||||
|
if (!user?.id) return null;
|
||||||
|
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const productId = Date.now();
|
||||||
|
|
||||||
|
await mockDB.products.add({
|
||||||
|
id: productId,
|
||||||
|
...productData,
|
||||||
|
shop_id: user.id,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now
|
||||||
|
});
|
||||||
|
|
||||||
|
if ("image_data" in productData && productData.image_data) {
|
||||||
|
const image_id = Date.now();
|
||||||
|
await mockDB.product_images.add({
|
||||||
|
id: image_id,
|
||||||
|
product_id: productId,
|
||||||
|
image_id: image_id,
|
||||||
|
image_url: productData.image_data,
|
||||||
|
alt_text: `${productData.name} image`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return mockDB.products.get(productId);
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateProduct(productId: number, data: Partial<ProductCreate>) {
|
||||||
|
const product = await mockDB.products.get(productId);
|
||||||
|
if (!product) return null;
|
||||||
|
|
||||||
|
const updatedProduct = {
|
||||||
|
...product,
|
||||||
|
...data,
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
};
|
||||||
|
await mockDB.products.put(updatedProduct);
|
||||||
|
|
||||||
|
if (data.image_data) {
|
||||||
|
const existingImage = await mockDB.product_images
|
||||||
|
.where("product_id")
|
||||||
|
.equals(productId)
|
||||||
|
.first();
|
||||||
|
if (existingImage) {
|
||||||
|
await mockDB.product_images.put({
|
||||||
|
...existingImage,
|
||||||
|
image_url: data.image_data,
|
||||||
|
alt_text: `${data.name ?? product.name} image`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const image_id = Date.now();
|
||||||
|
await mockDB.product_images.add({
|
||||||
|
id: image_id,
|
||||||
|
product_id: productId,
|
||||||
|
image_id: image_id,
|
||||||
|
image_url: data.image_data,
|
||||||
|
alt_text: `${data.name ?? product.name} image`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedProduct;
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteProduct(productId: number) {
|
||||||
|
await mockDB.product_images.where("product_id").equals(productId).delete();
|
||||||
|
await mockDB.product_variants
|
||||||
|
.where("product_id")
|
||||||
|
.equals(productId)
|
||||||
|
.delete();
|
||||||
|
await mockDB.product_category_junctions
|
||||||
|
.where("product_id")
|
||||||
|
.equals(productId)
|
||||||
|
.delete();
|
||||||
|
await mockDB.products.delete(productId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
41
frontend/src/api/mock/shop-mock-api.ts
Normal file
41
frontend/src/api/mock/shop-mock-api.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { ShopAPI } from "../api-definition";
|
||||||
|
import { mockDB } from "./db";
|
||||||
|
import { extractSubFromJWT } from "@/utils/jwt";
|
||||||
|
|
||||||
|
const db = mockDB;
|
||||||
|
|
||||||
|
export const MockShopAPI: ShopAPI = {
|
||||||
|
async getShop() {
|
||||||
|
const token = localStorage.getItem("access_token");
|
||||||
|
if (!token) return null;
|
||||||
|
|
||||||
|
const uuid = extractSubFromJWT(token);
|
||||||
|
if (!uuid) return null;
|
||||||
|
|
||||||
|
const user = await db.users.where("uuid").equals(uuid).first();
|
||||||
|
if (!user?.id) return null;
|
||||||
|
|
||||||
|
const dbShop = await db.shops.get(user.id);
|
||||||
|
return dbShop ?? null;
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateShop(data) {
|
||||||
|
const token = localStorage.getItem("access_token");
|
||||||
|
console.log("Token ->", token);
|
||||||
|
if (!token) return;
|
||||||
|
|
||||||
|
const uuid = extractSubFromJWT(token);
|
||||||
|
console.log("UUID ->", uuid);
|
||||||
|
if (!uuid) return;
|
||||||
|
|
||||||
|
const user = await db.users.where("uuid").equals(uuid).first();
|
||||||
|
console.log("User ->", user);
|
||||||
|
if (!user?.id) return;
|
||||||
|
|
||||||
|
console.log("Saving data ->", data);
|
||||||
|
await db.shops.update(user.id, {
|
||||||
|
...data,
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
13
frontend/src/api/mock/utils/currentUser.ts
Normal file
13
frontend/src/api/mock/utils/currentUser.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { mockDB } from "../db";
|
||||||
|
import { extractSubFromJWT } from "@/utils/jwt";
|
||||||
|
|
||||||
|
export async function getCurrentUserDirect() {
|
||||||
|
const token = localStorage.getItem("access_token");
|
||||||
|
if (!token) return null;
|
||||||
|
|
||||||
|
const uuid = extractSubFromJWT(token);
|
||||||
|
if (!uuid) return null;
|
||||||
|
|
||||||
|
const user = await mockDB.users.where("uuid").equals(uuid).first();
|
||||||
|
return user ?? null;
|
||||||
|
}
|
7
frontend/src/api/mock/utils/imageToBase64.ts
Normal file
7
frontend/src/api/mock/utils/imageToBase64.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export const fileToBase64 = (file: File): Promise<string> =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => resolve(reader.result as string);
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
19
frontend/src/api/real/auth-real-api.ts
Normal file
19
frontend/src/api/real/auth-real-api.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { DashboardService, LoginService, UserService } from "@/client";
|
||||||
|
import { AuthAPI } from "../api-definition";
|
||||||
|
|
||||||
|
export const RealAuthAPI: AuthAPI = {
|
||||||
|
async getCurrentUser() {
|
||||||
|
return DashboardService.userGetUser();
|
||||||
|
},
|
||||||
|
async registerUser(data) {
|
||||||
|
await DashboardService.userRegister({ requestBody: data });
|
||||||
|
},
|
||||||
|
async loginUser(data) {
|
||||||
|
return LoginService.dashboardLoginAccessToken({
|
||||||
|
formData: data.formData
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async updateUser(data) {
|
||||||
|
await UserService.userUpdateUser({ requestBody: data });
|
||||||
|
}
|
||||||
|
};
|
12
frontend/src/assets/placeholder.svg
Normal file
12
frontend/src/assets/placeholder.svg
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 120 120"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="120" height="120" />
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M33.2503 38.4816C33.2603 37.0472 34.4199 35.8864 35.8543 35.875H83.1463C84.5848 35.875 85.7503 37.0431 85.7503 38.4816V80.5184C85.7403 81.9528 84.5807 83.1136 83.1463 83.125H35.8543C34.4158 83.1236 33.2503 81.957 33.2503 80.5184V38.4816ZM80.5006 41.1251H38.5006V77.8751L62.8921 53.4783C63.9172 52.4536 65.5788 52.4536 66.6039 53.4783L80.5006 67.4013V41.1251ZM43.75 51.6249C43.75 54.5244 46.1005 56.8749 49 56.8749C51.8995 56.8749 54.25 54.5244 54.25 51.6249C54.25 48.7254 51.8995 46.3749 49 46.3749C46.1005 46.3749 43.75 48.7254 43.75 51.6249Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 763 B |
@ -4,17 +4,11 @@ import { useEffect, useState } from "react";
|
|||||||
|
|
||||||
import { handleServerError } from "@/utils/handle-server-error";
|
import { handleServerError } from "@/utils/handle-server-error";
|
||||||
import { ApiError } from "@/errors/api-error";
|
import { ApiError } from "@/errors/api-error";
|
||||||
import {
|
import { ShopLoginAccessTokenData, UserPublic } from "@/client";
|
||||||
DashboardService,
|
|
||||||
LoginService,
|
|
||||||
ShopLoginAccessTokenData,
|
|
||||||
UserPublic,
|
|
||||||
UserRegister,
|
|
||||||
UserService,
|
|
||||||
UserUpdate
|
|
||||||
} from "@/client";
|
|
||||||
import { toast } from "./useToast";
|
import { toast } from "./useToast";
|
||||||
|
|
||||||
|
import { authAPI } from "@/api/api";
|
||||||
|
|
||||||
const isLoggedIn = () => {
|
const isLoggedIn = () => {
|
||||||
return localStorage.getItem("access_token") !== null;
|
return localStorage.getItem("access_token") !== null;
|
||||||
};
|
};
|
||||||
@ -27,25 +21,19 @@ const useAuth = () => {
|
|||||||
|
|
||||||
const { data: user } = useQuery<UserPublic | null, Error>({
|
const { data: user } = useQuery<UserPublic | null, Error>({
|
||||||
queryKey: ["currentUser"],
|
queryKey: ["currentUser"],
|
||||||
queryFn: DashboardService.userGetUser,
|
queryFn: authAPI.getCurrentUser,
|
||||||
enabled: loggedIn
|
enabled: loggedIn
|
||||||
});
|
});
|
||||||
|
|
||||||
const signUpMutation = useMutation({
|
const signUpMutation = useMutation({
|
||||||
mutationFn: (data: UserRegister) =>
|
mutationFn: authAPI.registerUser,
|
||||||
DashboardService.userRegister({ requestBody: data }),
|
|
||||||
onSuccess: () => navigate({ to: "/sign-in" }),
|
onSuccess: () => navigate({ to: "/sign-in" }),
|
||||||
onError: (err: ApiError) => handleServerError(err),
|
onError: (err: ApiError) => handleServerError(err),
|
||||||
onSettled: () => queryClient.invalidateQueries({ queryKey: ["users"] })
|
onSettled: () => queryClient.invalidateQueries({ queryKey: ["users"] })
|
||||||
});
|
});
|
||||||
|
|
||||||
const login = async (data: ShopLoginAccessTokenData) => {
|
const login = async (data: ShopLoginAccessTokenData) => {
|
||||||
const response = await LoginService.dashboardLoginAccessToken({
|
const response = await authAPI.loginUser(data);
|
||||||
formData: {
|
|
||||||
username: data.formData.username,
|
|
||||||
password: data.formData.password
|
|
||||||
}
|
|
||||||
});
|
|
||||||
localStorage.setItem("access_token", response.access_token);
|
localStorage.setItem("access_token", response.access_token);
|
||||||
setLoggedIn(true);
|
setLoggedIn(true);
|
||||||
await queryClient.invalidateQueries({ queryKey: ["currentUser"] });
|
await queryClient.invalidateQueries({ queryKey: ["currentUser"] });
|
||||||
@ -65,8 +53,7 @@ const useAuth = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateAccountMutation = useMutation({
|
const updateAccountMutation = useMutation({
|
||||||
mutationFn: (data: UserUpdate) =>
|
mutationFn: authAPI.updateUser,
|
||||||
UserService.userUpdateUser({ requestBody: data }),
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast({ title: "Account updated successfully" });
|
toast({ title: "Account updated successfully" });
|
||||||
queryClient.invalidateQueries({ queryKey: ["currentUser"] });
|
queryClient.invalidateQueries({ queryKey: ["currentUser"] });
|
||||||
@ -77,7 +64,8 @@ const useAuth = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("Checking whether the token is valid");
|
console.log("Checking whether the token is valid");
|
||||||
const userLoggedInAndNull = loggedIn && user === null;
|
const userLoggedInAndNull = loggedIn && user === null;
|
||||||
const tokenExistsAndNull = Boolean(localStorage.getItem("access_token")) && user === null;
|
const tokenExistsAndNull =
|
||||||
|
Boolean(localStorage.getItem("access_token")) && user === null;
|
||||||
|
|
||||||
if (userLoggedInAndNull || tokenExistsAndNull) {
|
if (userLoggedInAndNull || tokenExistsAndNull) {
|
||||||
console.warn("User data is null while logged in, logging out.");
|
console.warn("User data is null while logged in, logging out.");
|
||||||
|
30
frontend/src/hooks/useProduct.ts
Normal file
30
frontend/src/hooks/useProduct.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { ProductCreate, ProductWithDetails } from "@/api/mock/models";
|
||||||
|
import { productsAPI } from "@/api/api";
|
||||||
|
|
||||||
|
export function useProduct(productId?: number) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const product = useQuery<ProductWithDetails | null>({
|
||||||
|
queryKey: ["product", productId],
|
||||||
|
queryFn: () => productsAPI.getProductById(productId!),
|
||||||
|
enabled: !!productId
|
||||||
|
});
|
||||||
|
|
||||||
|
const createProduct = useMutation({
|
||||||
|
mutationFn: (data: ProductCreate) => productsAPI.createProduct(data),
|
||||||
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["products"] })
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateProduct = useMutation({
|
||||||
|
mutationFn: (data: Partial<ProductCreate>) => productsAPI.updateProduct(productId!, data),
|
||||||
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["products"] })
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteProduct = useMutation({
|
||||||
|
mutationFn: () => productsAPI.deleteProduct(productId!),
|
||||||
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["products"] })
|
||||||
|
});
|
||||||
|
|
||||||
|
return { product, createProduct, updateProduct, deleteProduct };
|
||||||
|
}
|
13
frontend/src/hooks/useProducts.ts
Normal file
13
frontend/src/hooks/useProducts.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// src/hooks/useProducts.ts
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { productsAPI } from "@/api/api";
|
||||||
|
import { ProductWithDetails } from "@/api/mock/models";
|
||||||
|
|
||||||
|
export function useProducts() {
|
||||||
|
const query = useQuery<ProductWithDetails[]>({
|
||||||
|
queryKey: ["products"],
|
||||||
|
queryFn: productsAPI.getProductsForShop
|
||||||
|
});
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
import { shopAPI } from "@/api/api";
|
||||||
|
import { Shop } from "@/api/mock/models";
|
||||||
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
export function useShop() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: shop,
|
||||||
|
isLoading,
|
||||||
|
isError
|
||||||
|
} = useQuery<Shop | null>({
|
||||||
|
queryKey: ["shop"],
|
||||||
|
queryFn: shopAPI.getShop
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateShopMutation = useMutation({
|
||||||
|
mutationFn: shopAPI.updateShop,
|
||||||
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["shop"] })
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
shop,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
updateShop: updateShopMutation.mutate,
|
||||||
|
updateStatus: updateShopMutation.status
|
||||||
|
};
|
||||||
|
}
|
152
frontend/src/pages/products/components/product-dialog.tsx
Normal file
152
frontend/src/pages/products/components/product-dialog.tsx
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Placeholder from "@/assets/placeholder.svg?react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { UseFormReturn } from "react-hook-form";
|
||||||
|
import { ProductForm } from "../hooks/use-product-form";
|
||||||
|
|
||||||
|
interface ProductDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
form: UseFormReturn<ProductForm>;
|
||||||
|
onSubmit: (e?: React.BaseSyntheticEvent) => void;
|
||||||
|
onDelete?: () => void;
|
||||||
|
onImageUpload: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
submitLabel: string;
|
||||||
|
imagePreview?: string;
|
||||||
|
showDeleteButton?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProductDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
form,
|
||||||
|
onSubmit,
|
||||||
|
onDelete,
|
||||||
|
onImageUpload,
|
||||||
|
submitLabel,
|
||||||
|
imagePreview,
|
||||||
|
showDeleteButton = false
|
||||||
|
}: ProductDialogProps) {
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-2xl font-semibold">
|
||||||
|
{submitLabel} Product
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<form onSubmit={onSubmit} className="space-y-6">
|
||||||
|
{/* Main fields grid */}
|
||||||
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||||
|
{/* Name field */}
|
||||||
|
<div className="flex flex-col justify-between">
|
||||||
|
<Label htmlFor="name" className="mb-1">
|
||||||
|
Product Name
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
{...form.register("name")}
|
||||||
|
placeholder="Enter product name"
|
||||||
|
/>
|
||||||
|
{form.formState.errors.name && (
|
||||||
|
<p className="text-sm text-red-500">
|
||||||
|
{form.formState.errors.name.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Label htmlFor="price" className="mb-1">
|
||||||
|
Price
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="price"
|
||||||
|
type="number"
|
||||||
|
{...form.register("price", { valueAsNumber: true })}
|
||||||
|
placeholder="0.00"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Label htmlFor="stock_quantity" className="mb-1">
|
||||||
|
Stock
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="stock_quantity"
|
||||||
|
type="number"
|
||||||
|
{...form.register("stock_quantity", { valueAsNumber: true })}
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Image upload & preview */}
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<Label htmlFor="image">Image</Label>
|
||||||
|
{imagePreview ? (
|
||||||
|
<img
|
||||||
|
src={imagePreview}
|
||||||
|
alt="Preview"
|
||||||
|
className="mb-2 h-48 w-48 rounded-lg border object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Placeholder className="h-48 w-48 fill-gray-100 dark:fill-slate-900 text-slate-900 dark:text-zinc-500" />
|
||||||
|
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-muted-foreground select-none">
|
||||||
|
JPG or PNG only. Max size: 2MB.
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
|
id="image"
|
||||||
|
type="file"
|
||||||
|
accept="image/png, image/jpeg"
|
||||||
|
onChange={onImageUpload}
|
||||||
|
className="pt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description spanning full width */}
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<Label htmlFor="description">Description</Label>
|
||||||
|
<Textarea
|
||||||
|
id="description"
|
||||||
|
{...form.register("description")}
|
||||||
|
placeholder="Short product description"
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
{form.formState.errors.description && (
|
||||||
|
<p className="text-sm text-red-500">
|
||||||
|
{form.formState.errors.description.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* Action buttons */}
|
||||||
|
<DialogFooter className="flex justify-end space-x-3">
|
||||||
|
{showDeleteButton && onDelete && (
|
||||||
|
<Button variant="destructive" type="button" onClick={onDelete}>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button type="submit">{submitLabel}</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
93
frontend/src/pages/products/components/product-form.tsx
Normal file
93
frontend/src/pages/products/components/product-form.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { DialogFooter } from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { UseFormReturn } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
name: z.string().min(1, "Name is required"),
|
||||||
|
description: z.string().min(1, "Description is required"),
|
||||||
|
price: z.coerce.number().min(0),
|
||||||
|
stock_quantity: z.coerce.number().int().min(0),
|
||||||
|
image_data: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
type ProductForm = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
export function ProductForm({
|
||||||
|
onSubmit,
|
||||||
|
form,
|
||||||
|
imagePreview,
|
||||||
|
onImageUpload,
|
||||||
|
submitLabel
|
||||||
|
}: {
|
||||||
|
onSubmit: () => void;
|
||||||
|
form: UseFormReturn<typeof ProductForm>;
|
||||||
|
imagePreview?: string;
|
||||||
|
onImageUpload: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
submitLabel: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<form onSubmit={onSubmit} className="grid gap-4 py-4">
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="name">Product Name</Label>
|
||||||
|
<Input {...form.register("name")} placeholder="Product name" />
|
||||||
|
{form.formState.errors.name && (
|
||||||
|
<p className="text-sm text-red-500">
|
||||||
|
{form.formState.errors.name.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="description">Description</Label>
|
||||||
|
<Input
|
||||||
|
{...form.register("description")}
|
||||||
|
placeholder="Short description"
|
||||||
|
/>
|
||||||
|
{form.formState.errors.description && (
|
||||||
|
<p className="text-sm text-red-500">
|
||||||
|
{form.formState.errors.description.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="price">Price</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
{...form.register("price", { valueAsNumber: true })}
|
||||||
|
placeholder="0.00"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="stock">Stock</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
{...form.register("stock_quantity", { valueAsNumber: true })}
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="image">Image</Label>
|
||||||
|
<Input
|
||||||
|
id="image"
|
||||||
|
type="file"
|
||||||
|
accept="image/jpg,image/png"
|
||||||
|
onChange={onImageUpload}
|
||||||
|
/>
|
||||||
|
{form.watch("image_data") && (
|
||||||
|
<img
|
||||||
|
src={form.watch("image_data")}
|
||||||
|
alt="Preview"
|
||||||
|
className="mt-2 h-24 w-24 rounded border object-cover"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="submit">{submitLabel}</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
60
frontend/src/pages/products/components/product-list.tsx
Normal file
60
frontend/src/pages/products/components/product-list.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { ProductWithDetails } from "@/api/mock/models";
|
||||||
|
import Placeholder from "@/assets/placeholder.svg?react";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
|
||||||
|
export function ProductList({
|
||||||
|
products,
|
||||||
|
isLoading,
|
||||||
|
onClick
|
||||||
|
}: {
|
||||||
|
products: ProductWithDetails[];
|
||||||
|
isLoading: boolean;
|
||||||
|
onClick: (id: number) => void;
|
||||||
|
}) {
|
||||||
|
if (isLoading) return <p className="col-span-full">Loading...</p>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className="faded-bottom no-scrollbar grid grid-cols-1 gap-4 overflow-auto pb-16 pt-4 sm:grid-cols-2 md:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8">
|
||||||
|
{products.map((product) => (
|
||||||
|
<Card
|
||||||
|
key={product.id}
|
||||||
|
onClick={() => onClick(product.id)}
|
||||||
|
className="cursor-pointer overflow-hidden">
|
||||||
|
<div className="relative aspect-square w-full overflow-hidden">
|
||||||
|
{product.images.length > 0 ? (
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
product.images.length > 0
|
||||||
|
? product.images[0].image_url
|
||||||
|
: "/images/placeholder.svg"
|
||||||
|
}
|
||||||
|
alt={product.name}
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Placeholder className="h-full w-full object-cover fill-gray-100 dark:fill-slate-900 text-slate-900 dark:text-zinc-500" />
|
||||||
|
)}
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-background/90 to-background/0 p-2">
|
||||||
|
<span className="rounded bg-secondary px-2 py-1 text-[10px] font-medium">
|
||||||
|
{product.stock_quantity} in stock
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CardHeader className="p-3 pb-2">
|
||||||
|
<CardTitle className="line-clamp-1 text-sm">
|
||||||
|
{product.name}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="line-clamp-2 text-xs text-muted-foreground">
|
||||||
|
{product.description}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
@ -1,110 +0,0 @@
|
|||||||
import {
|
|
||||||
IconBrandDiscord,
|
|
||||||
IconBrandDocker,
|
|
||||||
IconBrandFigma,
|
|
||||||
IconBrandGithub,
|
|
||||||
IconBrandGitlab,
|
|
||||||
IconBrandGmail,
|
|
||||||
IconBrandMedium,
|
|
||||||
IconBrandNotion,
|
|
||||||
IconBrandSkype,
|
|
||||||
IconBrandSlack,
|
|
||||||
IconBrandStripe,
|
|
||||||
IconBrandTelegram,
|
|
||||||
IconBrandTrello,
|
|
||||||
IconBrandWhatsapp,
|
|
||||||
IconBrandZoom
|
|
||||||
} from "@tabler/icons-react";
|
|
||||||
|
|
||||||
export const apps = [
|
|
||||||
{
|
|
||||||
name: "Telegram",
|
|
||||||
logo: <IconBrandTelegram />,
|
|
||||||
connected: false,
|
|
||||||
desc: "Connect with Telegram for real-time communication."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Notion",
|
|
||||||
logo: <IconBrandNotion />,
|
|
||||||
connected: true,
|
|
||||||
desc: "Effortlessly sync Notion pages for seamless collaboration."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Figma",
|
|
||||||
logo: <IconBrandFigma />,
|
|
||||||
connected: true,
|
|
||||||
desc: "View and collaborate on Figma designs in one place."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Trello",
|
|
||||||
logo: <IconBrandTrello />,
|
|
||||||
connected: false,
|
|
||||||
desc: "Sync Trello cards for streamlined project management."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Slack",
|
|
||||||
logo: <IconBrandSlack />,
|
|
||||||
connected: false,
|
|
||||||
desc: "Integrate Slack for efficient team communication"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Zoom",
|
|
||||||
logo: <IconBrandZoom />,
|
|
||||||
connected: true,
|
|
||||||
desc: "Host Zoom meetings directly from the dashboard."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Stripe",
|
|
||||||
logo: <IconBrandStripe />,
|
|
||||||
connected: false,
|
|
||||||
desc: "Easily manage Stripe transactions and payments."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Gmail",
|
|
||||||
logo: <IconBrandGmail />,
|
|
||||||
connected: true,
|
|
||||||
desc: "Access and manage Gmail messages effortlessly."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Medium",
|
|
||||||
logo: <IconBrandMedium />,
|
|
||||||
connected: false,
|
|
||||||
desc: "Explore and share Medium stories on your dashboard."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Skype",
|
|
||||||
logo: <IconBrandSkype />,
|
|
||||||
connected: false,
|
|
||||||
desc: "Connect with Skype contacts seamlessly."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Docker",
|
|
||||||
logo: <IconBrandDocker />,
|
|
||||||
connected: false,
|
|
||||||
desc: "Effortlessly manage Docker containers on your dashboard."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "GitHub",
|
|
||||||
logo: <IconBrandGithub />,
|
|
||||||
connected: false,
|
|
||||||
desc: "Streamline code management with GitHub integration."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "GitLab",
|
|
||||||
logo: <IconBrandGitlab />,
|
|
||||||
connected: false,
|
|
||||||
desc: "Efficiently manage code projects with GitLab integration."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Discord",
|
|
||||||
logo: <IconBrandDiscord />,
|
|
||||||
connected: false,
|
|
||||||
desc: "Connect with Discord for seamless team communication."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "WhatsApp",
|
|
||||||
logo: <IconBrandWhatsapp />,
|
|
||||||
connected: false,
|
|
||||||
desc: "Easily integrate WhatsApp for direct messaging."
|
|
||||||
}
|
|
||||||
];
|
|
43
frontend/src/pages/products/hooks/use-product-form.tsx
Normal file
43
frontend/src/pages/products/hooks/use-product-form.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { ProductWithDetails } from "@/api/mock/models";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
|
||||||
|
export const schema = z.object({
|
||||||
|
name: z.string().min(1, "Name is required"),
|
||||||
|
description: z.string().min(1, "Description is required"),
|
||||||
|
price: z.coerce.number().min(0),
|
||||||
|
stock_quantity: z.coerce.number().int().min(0),
|
||||||
|
image_data: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ProductForm = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
export function useProductForm(productData?: ProductWithDetails | null) {
|
||||||
|
const form = useForm<ProductForm>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
price: 0,
|
||||||
|
stock_quantity: 0,
|
||||||
|
image_data: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (productData) {
|
||||||
|
form.reset({
|
||||||
|
name: productData.name,
|
||||||
|
description: productData.description,
|
||||||
|
price: productData.price,
|
||||||
|
stock_quantity: productData.stock_quantity,
|
||||||
|
image_data: productData.images[0]?.image_url || ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [productData, form]);
|
||||||
|
|
||||||
|
return form;
|
||||||
|
}
|
@ -1,55 +1,96 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import {
|
import {
|
||||||
IconAdjustmentsHorizontal,
|
IconAdjustmentsHorizontal,
|
||||||
IconSortAscendingLetters,
|
IconSortAscendingLetters,
|
||||||
IconSortDescendingLetters
|
IconSortDescendingLetters,
|
||||||
|
IconPlus
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
|
|
||||||
|
import { Header } from "@/components/layout/header";
|
||||||
|
import { Main } from "@/components/layout/main";
|
||||||
|
import { Search } from "@/components/search";
|
||||||
|
import { ThemeSwitch } from "@/components/theme-switch";
|
||||||
|
import { ProfileDropdown } from "@/components/profile-dropdown";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue
|
SelectValue,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Header } from "@/components/layout/header";
|
|
||||||
import { Main } from "@/components/layout/main";
|
|
||||||
import { ProfileDropdown } from "@/components/profile-dropdown";
|
|
||||||
import { Search } from "@/components/search";
|
|
||||||
import { ThemeSwitch } from "@/components/theme-switch";
|
|
||||||
import { apps } from "./data/apps";
|
|
||||||
|
|
||||||
const appText = new Map<string, string>([
|
import { useProducts } from "@/hooks/useProducts";
|
||||||
["all", "All Apps"],
|
import { useProduct } from "@/hooks/useProduct";
|
||||||
["connected", "Connected"],
|
import { useState } from "react";
|
||||||
["notConnected", "Not Connected"]
|
import { useProductForm } from "./hooks/use-product-form";
|
||||||
]);
|
import { ProductList } from "./components/product-list";
|
||||||
|
import { ProductDialog } from "./components/product-dialog";
|
||||||
|
import { toast } from "@/hooks/useToast";
|
||||||
|
|
||||||
export default function Products() {
|
export default function Products() {
|
||||||
const [sort, setSort] = useState("ascending");
|
const [sort, setSort] = useState<"ascending" | "descending">("ascending");
|
||||||
const [appType, setAppType] = useState("all");
|
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||||
|
const [selectedProductId, setSelectedProductId] = useState<
|
||||||
|
number | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
const filteredApps = apps
|
const { data: products = [], isLoading } = useProducts();
|
||||||
.sort((a, b) =>
|
const { product, createProduct, updateProduct, deleteProduct } =
|
||||||
sort === "ascending"
|
useProduct(selectedProductId);
|
||||||
? a.name.localeCompare(b.name)
|
|
||||||
: b.name.localeCompare(a.name)
|
const form = useProductForm(product.data);
|
||||||
)
|
|
||||||
.filter((app) =>
|
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
appType === "connected"
|
const file = e.target.files?.[0];
|
||||||
? app.connected
|
if (!file) return;
|
||||||
: appType === "notConnected"
|
const reader = new FileReader();
|
||||||
? !app.connected
|
reader.onload = () => form.setValue("image_data", reader.result as string);
|
||||||
: true
|
reader.readAsDataURL(file);
|
||||||
)
|
};
|
||||||
.filter((app) => app.name.toLowerCase().includes(searchTerm.toLowerCase()));
|
|
||||||
|
const handleCreateSubmit = form.handleSubmit((data) => {
|
||||||
|
form.reset({
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
price: 0,
|
||||||
|
stock_quantity: 0,
|
||||||
|
image_data: ""
|
||||||
|
});
|
||||||
|
createProduct.mutate(data);
|
||||||
|
setDialogOpen(false);
|
||||||
|
toast({ title: `${data.name} created`, variant: "default" });
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleEditSubmit = form.handleSubmit((data) => {
|
||||||
|
if (!selectedProductId) return;
|
||||||
|
updateProduct.mutate(data);
|
||||||
|
setEditDialogOpen(false);
|
||||||
|
toast({ title: `${data.name} saved`, variant: "default" });
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
if (!selectedProductId) return;
|
||||||
|
deleteProduct.mutate();
|
||||||
|
setEditDialogOpen(false);
|
||||||
|
toast({ title: `${product.data?.name} deleted`, variant: "destructive" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortedProducts = [...products].sort((a, b) =>
|
||||||
|
sort === "ascending"
|
||||||
|
? a.name.localeCompare(b.name)
|
||||||
|
: b.name.localeCompare(a.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredProducts = sortedProducts.filter((product) =>
|
||||||
|
product.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* ===== Top Heading ===== */}
|
|
||||||
<Header>
|
<Header>
|
||||||
<Search />
|
<Search />
|
||||||
<div className="ml-auto flex items-center gap-4">
|
<div className="ml-auto flex items-center gap-4">
|
||||||
@ -58,37 +99,26 @@ export default function Products() {
|
|||||||
</div>
|
</div>
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
{/* ===== Content ===== */}
|
|
||||||
<Main fixed>
|
<Main fixed>
|
||||||
<div>
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="text-2xl font-bold tracking-tight">
|
<div>
|
||||||
Products
|
<h1 className="text-2xl font-bold tracking-tight">Products</h1>
|
||||||
</h1>
|
<p className="text-muted-foreground">
|
||||||
<p className="text-muted-foreground">
|
Here's a list of your products!
|
||||||
Here's a list of your products!
|
</p>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="my-4 flex items-end justify-between sm:my-0 sm:items-center">
|
|
||||||
<div className="flex flex-col gap-4 sm:my-4 sm:flex-row">
|
|
||||||
<Input
|
|
||||||
placeholder="Filter apps..."
|
|
||||||
className="h-9 w-40 lg:w-[250px]"
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
/>
|
|
||||||
<Select value={appType} onValueChange={setAppType}>
|
|
||||||
<SelectTrigger className="w-36">
|
|
||||||
<SelectValue>{appText.get(appType)}</SelectValue>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">All Apps</SelectItem>
|
|
||||||
<SelectItem value="connected">Connected</SelectItem>
|
|
||||||
<SelectItem value="notConnected">Not Connected</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Select value={sort} onValueChange={setSort}>
|
<div className="flex flex-col gap-4 sm:my-4 sm:flex-row">
|
||||||
|
<Input
|
||||||
|
placeholder="Search products..."
|
||||||
|
className="h-9 w-40 lg:w-[250px]"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={sort}
|
||||||
|
onValueChange={(v) => setSort(v as "ascending" | "descending")}>
|
||||||
<SelectTrigger className="w-16">
|
<SelectTrigger className="w-16">
|
||||||
<SelectValue>
|
<SelectValue>
|
||||||
<IconAdjustmentsHorizontal size={18} />
|
<IconAdjustmentsHorizontal size={18} />
|
||||||
@ -110,32 +140,49 @@ export default function Products() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator className="shadow" />
|
<Separator className="shadow" />
|
||||||
<ul className="faded-bottom no-scrollbar grid gap-4 overflow-auto pb-16 pt-4 md:grid-cols-2 lg:grid-cols-3">
|
|
||||||
{filteredApps.map((app) => (
|
<ProductList
|
||||||
<li
|
products={filteredProducts}
|
||||||
key={app.name}
|
isLoading={isLoading}
|
||||||
className="rounded-lg border p-4 hover:shadow-md">
|
onClick={(id) => {
|
||||||
<div className="mb-8 flex items-center justify-between">
|
setSelectedProductId(id);
|
||||||
<div
|
setEditDialogOpen(true);
|
||||||
className={`flex size-10 items-center justify-center rounded-lg bg-muted p-2`}>
|
}}
|
||||||
{app.logo}
|
/>
|
||||||
</div>
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
onClick={() => setDialogOpen(true)}
|
||||||
size="sm"
|
className="absolute bottom-0 z-10 mb-4 size-14 rounded-full p-0 shadow-lg hover:brightness-110"
|
||||||
className={`${app.connected ? "border border-blue-300 bg-blue-50 hover:bg-blue-100 dark:border-blue-700 dark:bg-blue-950 dark:hover:bg-blue-900" : ""}`}>
|
variant="default">
|
||||||
{app.connected ? "Connected" : "Connect"}
|
<IconPlus size={28} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 className="mb-1 font-semibold">{app.name}</h2>
|
|
||||||
<p className="line-clamp-2 text-gray-500">{app.desc}</p>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</Main>
|
</Main>
|
||||||
|
|
||||||
|
{/* Create Dialog */}
|
||||||
|
<ProductDialog
|
||||||
|
open={dialogOpen}
|
||||||
|
onOpenChange={setDialogOpen}
|
||||||
|
form={form}
|
||||||
|
onSubmit={handleCreateSubmit}
|
||||||
|
onImageUpload={handleImageUpload}
|
||||||
|
submitLabel="Create"
|
||||||
|
imagePreview={form.watch("image_data")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Edit Dialog */}
|
||||||
|
<ProductDialog
|
||||||
|
open={editDialogOpen}
|
||||||
|
onOpenChange={setEditDialogOpen}
|
||||||
|
form={form}
|
||||||
|
onSubmit={handleEditSubmit}
|
||||||
|
onImageUpload={handleImageUpload}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
submitLabel="Update"
|
||||||
|
imagePreview={form.watch("image_data")}
|
||||||
|
showDeleteButton
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,9 @@ import { PhoneInput } from "@/components/ui/phone-number-input";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { useShop } from "@/hooks/useShop";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { toast } from "@/hooks/useToast";
|
||||||
|
|
||||||
const shopAboutSchema = z.object({
|
const shopAboutSchema = z.object({
|
||||||
name: z.string().min(1, "Name is required"),
|
name: z.string().min(1, "Name is required"),
|
||||||
@ -25,7 +28,7 @@ const shopAboutSchema = z.object({
|
|||||||
address: z.object({
|
address: z.object({
|
||||||
street: z.string(),
|
street: z.string(),
|
||||||
city: z.string(),
|
city: z.string(),
|
||||||
state: z.string().optional(),
|
state: z.string().nullable().optional(),
|
||||||
postal_code: z.string(),
|
postal_code: z.string(),
|
||||||
country: z.string()
|
country: z.string()
|
||||||
}),
|
}),
|
||||||
@ -35,31 +38,24 @@ const shopAboutSchema = z.object({
|
|||||||
type ShopAboutFormValues = z.infer<typeof shopAboutSchema>;
|
type ShopAboutFormValues = z.infer<typeof shopAboutSchema>;
|
||||||
|
|
||||||
export function ShopAboutForm() {
|
export function ShopAboutForm() {
|
||||||
const defaultValues: ShopAboutFormValues = {
|
const { shop, isLoading, updateShop } = useShop();
|
||||||
name: "My Shop",
|
|
||||||
description: "This is a sample shop description.",
|
|
||||||
currency: "USD",
|
|
||||||
contact_email: "user@example.com",
|
|
||||||
contact_phone_number: "+266018975510",
|
|
||||||
address: {
|
|
||||||
street: "123 Main St",
|
|
||||||
city: "Metropolis",
|
|
||||||
state: "",
|
|
||||||
postal_code: "12345",
|
|
||||||
country: "USA"
|
|
||||||
},
|
|
||||||
status: "inactive"
|
|
||||||
};
|
|
||||||
|
|
||||||
const form = useForm<ShopAboutFormValues>({
|
const form = useForm<ShopAboutFormValues>({
|
||||||
resolver: zodResolver(shopAboutSchema),
|
resolver: zodResolver(shopAboutSchema),
|
||||||
defaultValues,
|
defaultValues: shop ?? undefined,
|
||||||
mode: "onChange"
|
mode: "onChange"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (shop) form.reset(shop);
|
||||||
|
}, [shop, form]);
|
||||||
|
|
||||||
function onSubmit(data: ShopAboutFormValues) {
|
function onSubmit(data: ShopAboutFormValues) {
|
||||||
console.log("Submitted shop about data:", data);
|
updateShop(data);
|
||||||
|
toast({title: "Saved shop data"})
|
||||||
}
|
}
|
||||||
|
if (isLoading) return <div className="p-4">Loading...</div>;
|
||||||
|
if (!shop) return <div className="p-4">No shop found</div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
@ -67,9 +63,7 @@ export function ShopAboutForm() {
|
|||||||
type="always"
|
type="always"
|
||||||
className="hidden w-full min-w-40 bg-background px-1 md:block">
|
className="hidden w-full min-w-40 bg-background px-1 md:block">
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="space-y-6">
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Basic Details</CardTitle>
|
<CardTitle className="text-xl">Basic Details</CardTitle>
|
||||||
@ -123,39 +117,37 @@ export function ShopAboutForm() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
<FormField
|
control={form.control}
|
||||||
control={form.control}
|
name="contact_email"
|
||||||
name="contact_email"
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<FormItem>
|
||||||
<FormItem>
|
<FormLabel>Contact Email</FormLabel>
|
||||||
<FormLabel>Contact Email</FormLabel>
|
<FormControl>
|
||||||
<FormControl>
|
<Input
|
||||||
<Input
|
type="email"
|
||||||
type="email"
|
placeholder="user@example.com"
|
||||||
placeholder="user@example.com"
|
{...field}
|
||||||
{...field}
|
/>
|
||||||
/>
|
</FormControl>
|
||||||
</FormControl>
|
<FormMessage />
|
||||||
<FormMessage />
|
</FormItem>
|
||||||
</FormItem>
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
<FormField
|
||||||
<FormField
|
control={form.control}
|
||||||
control={form.control}
|
name="contact_phone_number"
|
||||||
name="contact_phone_number"
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<FormItem>
|
||||||
<FormItem>
|
<FormLabel>Phone Number</FormLabel>
|
||||||
<FormLabel>Phone Number</FormLabel>
|
<FormControl>
|
||||||
<FormControl>
|
<PhoneInput {...field} />
|
||||||
<PhoneInput {...field} />
|
</FormControl>
|
||||||
</FormControl>
|
<FormMessage />
|
||||||
<FormMessage />
|
</FormItem>
|
||||||
</FormItem>
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@ -198,7 +190,7 @@ export function ShopAboutForm() {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>State/Province</FormLabel>
|
<FormLabel>State/Province</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="State" {...field} />
|
<Input placeholder="State" {...field} value={field.value ?? ""} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
16
frontend/src/utils/jwt.ts
Normal file
16
frontend/src/utils/jwt.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export function generateFakeJWT(sub: string): string {
|
||||||
|
const header = btoa(JSON.stringify({ alg: "none", typ: "JWT" }));
|
||||||
|
const payload = btoa(JSON.stringify({ sub }));
|
||||||
|
const signature = "";
|
||||||
|
return `${header}.${payload}.${signature}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractSubFromJWT(token: string): string | null {
|
||||||
|
try {
|
||||||
|
const [, payload] = token.split(".");
|
||||||
|
const decoded = JSON.parse(atob(payload));
|
||||||
|
return decoded.sub ?? null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
1
frontend/src/vite-env.d.ts
vendored
1
frontend/src/vite-env.d.ts
vendored
@ -1 +1,2 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
/// <reference types="vite-plugin-svgr/client" />
|
||||||
|
@ -2,11 +2,13 @@ import path from "path"
|
|||||||
import { defineConfig } from "vite"
|
import { defineConfig } from "vite"
|
||||||
import viteReact from '@vitejs/plugin-react'
|
import viteReact from '@vitejs/plugin-react'
|
||||||
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
|
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
|
||||||
|
import vitePluginSvgr from "vite-plugin-svgr";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
TanStackRouterVite({ autoCodeSplitting: true }),
|
TanStackRouterVite({ autoCodeSplitting: true }),
|
||||||
viteReact()
|
viteReact(),
|
||||||
|
vitePluginSvgr()
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user