From c60ec969d54e58d827462fda4c1dde127c3e477c Mon Sep 17 00:00:00 2001 From: Thastertyn Date: Sat, 19 Apr 2025 11:02:13 +0200 Subject: [PATCH] Products are nearly finished, also switched to mock api --- frontend/package.json | 7 +- frontend/pnpm-lock.yaml | 298 +++++++++++++++++- frontend/src/api/api-definition.ts | 19 ++ frontend/src/api/api.ts | 12 + frontend/src/api/mock/auth-mock-api.ts | 121 +++++++ frontend/src/api/mock/db.ts | 41 +++ frontend/src/api/mock/models.ts | 107 +++++++ frontend/src/api/mock/products-mock-api.ts | 163 ++++++++++ frontend/src/api/mock/shop-mock-api.ts | 41 +++ frontend/src/api/mock/utils/currentUser.ts | 13 + frontend/src/api/mock/utils/imageToBase64.ts | 7 + frontend/src/api/real/auth-real-api.ts | 19 ++ frontend/src/assets/placeholder.svg | 12 + frontend/src/hooks/useAuth.ts | 34 +- frontend/src/hooks/useProduct.ts | 30 ++ frontend/src/hooks/useProducts.ts | 13 + frontend/src/hooks/useShop.ts | 29 ++ .../products/components/product-dialog.tsx | 152 +++++++++ .../products/components/product-form.tsx | 93 ++++++ .../products/components/product-list.tsx | 60 ++++ frontend/src/pages/products/data/apps.tsx | 110 ------- .../pages/products/hooks/use-product-form.tsx | 43 +++ frontend/src/pages/products/index.tsx | 217 ++++++++----- .../pages/shop/components/shop-about-form.tsx | 100 +++--- frontend/src/utils/jwt.ts | 16 + frontend/src/vite-env.d.ts | 1 + frontend/vite.config.ts | 4 +- 27 files changed, 1486 insertions(+), 276 deletions(-) create mode 100644 frontend/src/api/api-definition.ts create mode 100644 frontend/src/api/api.ts create mode 100644 frontend/src/api/mock/auth-mock-api.ts create mode 100644 frontend/src/api/mock/db.ts create mode 100644 frontend/src/api/mock/models.ts create mode 100644 frontend/src/api/mock/products-mock-api.ts create mode 100644 frontend/src/api/mock/shop-mock-api.ts create mode 100644 frontend/src/api/mock/utils/currentUser.ts create mode 100644 frontend/src/api/mock/utils/imageToBase64.ts create mode 100644 frontend/src/api/real/auth-real-api.ts create mode 100644 frontend/src/assets/placeholder.svg create mode 100644 frontend/src/hooks/useProduct.ts create mode 100644 frontend/src/hooks/useProducts.ts create mode 100644 frontend/src/pages/products/components/product-dialog.tsx create mode 100644 frontend/src/pages/products/components/product-form.tsx create mode 100644 frontend/src/pages/products/components/product-list.tsx delete mode 100644 frontend/src/pages/products/data/apps.tsx create mode 100644 frontend/src/pages/products/hooks/use-product-form.tsx create mode 100644 frontend/src/utils/jwt.ts diff --git a/frontend/package.json b/frontend/package.json index 4175883..5fc70f6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,7 +29,7 @@ "@radix-ui/react-scroll-area": "^1.2.1", "@radix-ui/react-select": "^2.1.2", "@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-tabs": "^1.1.1", "@radix-ui/react-toast": "^1.2.2", @@ -45,6 +45,7 @@ "clsx": "^2.1.1", "cmdk": "^1.0.4", "date-fns": "^3.6.0", + "dexie": "^4.0.11", "js-cookie": "^3.0.5", "jwt-decode": "^4.0.0", "lucide-react": "^0.475.0", @@ -56,6 +57,7 @@ "recharts": "^2.14.1", "tailwind-merge": "^3.0.1", "tailwindcss-animate": "^1.0.7", + "uuid": "^11.1.0", "zod": "^3.24.2", "zustand": "^5.0.3" }, @@ -86,6 +88,7 @@ "tailwindcss": "^3.4.17", "typescript": "~5.7.2", "typescript-eslint": "^8.22.0", - "vite": "^6.1.0" + "vite": "^6.1.0", + "vite-plugin-svgr": "^4.3.0" } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index b61c04e..54ba0f2 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -54,7 +54,7 @@ importers: 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) '@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) '@radix-ui/react-switch': specifier: ^1.1.1 @@ -101,6 +101,9 @@ importers: date-fns: specifier: ^3.6.0 version: 3.6.0 + dexie: + specifier: ^4.0.11 + version: 4.0.11 js-cookie: specifier: ^3.0.5 version: 3.0.5 @@ -134,6 +137,9 @@ importers: tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@3.4.17) + uuid: + specifier: ^11.1.0 + version: 11.1.0 zod: specifier: ^3.24.2 version: 3.24.2 @@ -222,6 +228,9 @@ importers: vite: 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) + 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: @@ -1147,6 +1156,15 @@ packages: '@radix-ui/rect@1.1.1': 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': resolution: {integrity: sha512-lGVys55Qb00Wvh8DMAocp5kIcaNzEFTmGhfFd88LfaogYTRKrdxgtlO5H6S49v2Nd8R2C6wLOal0qv6/kCkOwA==} cpu: [arm] @@ -1247,6 +1265,74 @@ packages: cpu: [x64] 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': resolution: {integrity: sha512-K6AntdUlNMQg8aChqjeXwnVhK6d4WRZ9TgtLSTmdU0Ugll4an7QK49s9NrT7XQU91cEsVvzdr++p1bNImx0hJg==} engines: {node: '>=10'} @@ -1691,6 +1777,10 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + caniuse-lite@1.0.30001713: resolution: {integrity: sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==} @@ -1765,6 +1855,15 @@ packages: convert-source-map@2.0.0: 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: resolution: {integrity: sha512-z+Uzesi8u8IdkViqqbzzbkf3+a7WJpcET5B7sPwTg7GXqPYpVEgNlZ/FC3l8KO4mEf+mNkmzKLppKTN4PlCJEQ==} @@ -1864,6 +1963,9 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + dexie@4.0.11: + resolution: {integrity: sha512-SOKO002EqlvBYYKQSew3iymBoN2EQ4BDw/3yprjh7kAfFzjBYkaMNa/pZvcA7HSWlcKSQb9XhPe3wKyQ0x4A8A==} + didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -1877,6 +1979,9 @@ packages: dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + dotenv@16.4.7: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} @@ -1904,6 +2009,13 @@ packages: resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} 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: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -1982,6 +2094,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -2170,6 +2285,9 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -2235,6 +2353,9 @@ packages: json-buffer@3.0.1: 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: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -2289,6 +2410,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -2371,6 +2495,9 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + node-fetch-native@1.6.6: resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==} @@ -2420,6 +2547,10 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + parse-ms@4.0.0: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} @@ -2439,6 +2570,10 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} @@ -2760,6 +2895,9 @@ packages: resolution: {integrity: sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ==} engines: {node: '>= 18'} + snake-case@3.0.4: + resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + solid-js@1.9.5: resolution: {integrity: sha512-ogI3DaFcyn6UhYhrgcyRAMbu/buBJitYQASZz5WzfQVPP10RD2AbCoRZ517psnezrasyCbWzIxZ6kVqet768xw==} @@ -2812,6 +2950,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svg-parser@2.0.4: + resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} + tailwind-merge@3.2.0: resolution: {integrity: sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==} @@ -2942,9 +3083,18 @@ packages: util-deprecate@1.0.2: 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: 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: resolution: {integrity: sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -3979,6 +4129,14 @@ snapshots: '@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': optional: true @@ -4039,6 +4197,76 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.39.0': 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': optional: true @@ -4523,6 +4751,8 @@ snapshots: camelcase-css@2.0.1: {} + camelcase@6.3.0: {} + caniuse-lite@1.0.30001713: {} chalk@4.1.2: @@ -4597,6 +4827,15 @@ snapshots: 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: {} cross-spawn@7.0.6: @@ -4674,6 +4913,8 @@ snapshots: detect-node-es@1.1.0: {} + dexie@4.0.11: {} + didyoumean@1.2.2: {} diff@7.0.0: {} @@ -4685,6 +4926,11 @@ snapshots: '@babel/runtime': 7.27.0 csstype: 3.1.3 + dot-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + dotenv@16.4.7: {} dunder-proto@1.0.1: @@ -4712,6 +4958,12 @@ snapshots: graceful-fs: 4.2.11 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-errors@1.3.0: {} @@ -4834,6 +5086,8 @@ snapshots: estraverse@5.3.0: {} + estree-walker@2.0.2: {} + esutils@2.0.3: {} eventemitter3@4.0.7: {} @@ -5011,6 +5265,8 @@ snapshots: internmap@2.0.3: {} + is-arrayish@0.2.1: {} + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -5057,6 +5313,8 @@ snapshots: json-buffer@3.0.1: {} + json-parse-even-better-errors@2.3.1: {} + json-schema-traverse@0.4.1: {} json-stable-stringify-without-jsonify@1.0.1: {} @@ -5111,6 +5369,10 @@ snapshots: dependencies: js-tokens: 4.0.0 + lower-case@2.0.2: + dependencies: + tslib: 2.8.1 + lru-cache@10.4.3: {} lru-cache@5.1.1: @@ -5182,6 +5444,11 @@ snapshots: 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-releases@2.0.19: {} @@ -5228,6 +5495,13 @@ snapshots: dependencies: 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: {} path-exists@4.0.0: {} @@ -5241,6 +5515,8 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-type@4.0.0: {} + pathe@1.1.2: {} pathe@2.0.3: {} @@ -5502,6 +5778,11 @@ snapshots: 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: dependencies: csstype: 3.1.3 @@ -5554,6 +5835,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svg-parser@2.0.4: {} + tailwind-merge@3.2.0: {} tailwindcss-animate@1.0.7(tailwindcss@3.4.17): @@ -5693,6 +5976,8 @@ snapshots: util-deprecate@1.0.2: {} + uuid@11.1.0: {} + victory-vendor@36.9.2: dependencies: '@types/d3-array': 3.2.1 @@ -5710,6 +5995,17 @@ snapshots: d3-time: 3.1.0 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): dependencies: esbuild: 0.25.2 diff --git a/frontend/src/api/api-definition.ts b/frontend/src/api/api-definition.ts new file mode 100644 index 0000000..cb04c44 --- /dev/null +++ b/frontend/src/api/api-definition.ts @@ -0,0 +1,19 @@ +import { + UserPublic, + UserRegister, + UserUpdate, + ShopLoginAccessTokenData +} from "@/client"; +import { Shop } from "./mock/models"; + +export interface AuthAPI { + getCurrentUser(): Promise; + registerUser(data: UserRegister): Promise; + loginUser(data: ShopLoginAccessTokenData): Promise<{ access_token: string }>; + updateUser(data: UserUpdate): Promise; +} + +export interface ShopAPI { + getShop(): Promise; + updateShop(data: Partial): Promise; +} diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts new file mode 100644 index 0000000..a1b8f7b --- /dev/null +++ b/frontend/src/api/api.ts @@ -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; \ No newline at end of file diff --git a/frontend/src/api/mock/auth-mock-api.ts b/frontend/src/api/mock/auth-mock-api.ts new file mode 100644 index 0000000..7eaef29 --- /dev/null +++ b/frontend/src/api/mock/auth-mock-api.ts @@ -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 + }); + } +}; diff --git a/frontend/src/api/mock/db.ts b/frontend/src/api/mock/db.ts new file mode 100644 index 0000000..78c50fd --- /dev/null +++ b/frontend/src/api/mock/db.ts @@ -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; + preferences!: Table; + statistics!: Table; + shops!: Table; + products!: Table; + product_variants!: Table; + product_images!: Table; + product_categories!: Table; + product_category_junctions!: Table; + + 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(); diff --git a/frontend/src/api/mock/models.ts b/frontend/src/api/mock/models.ts new file mode 100644 index 0000000..5540342 --- /dev/null +++ b/frontend/src/api/mock/models.ts @@ -0,0 +1,107 @@ +import { UserRegister } from "@/client"; + +export type UserRole = "owner" | "customer" | "employee" | "manager" | "admin"; + +export interface MockUser extends Omit { + 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[]; +} diff --git a/frontend/src/api/mock/products-mock-api.ts b/frontend/src/api/mock/products-mock-api.ts new file mode 100644 index 0000000..bf8e7de --- /dev/null +++ b/frontend/src/api/mock/products-mock-api.ts @@ -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 { + 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 { + 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) { + 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; + } +}; diff --git a/frontend/src/api/mock/shop-mock-api.ts b/frontend/src/api/mock/shop-mock-api.ts new file mode 100644 index 0000000..e6ff7fe --- /dev/null +++ b/frontend/src/api/mock/shop-mock-api.ts @@ -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() + }); + } +}; diff --git a/frontend/src/api/mock/utils/currentUser.ts b/frontend/src/api/mock/utils/currentUser.ts new file mode 100644 index 0000000..1baa0bc --- /dev/null +++ b/frontend/src/api/mock/utils/currentUser.ts @@ -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; +} \ No newline at end of file diff --git a/frontend/src/api/mock/utils/imageToBase64.ts b/frontend/src/api/mock/utils/imageToBase64.ts new file mode 100644 index 0000000..d537721 --- /dev/null +++ b/frontend/src/api/mock/utils/imageToBase64.ts @@ -0,0 +1,7 @@ +export const fileToBase64 = (file: File): Promise => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = reject; + reader.readAsDataURL(file); + }); diff --git a/frontend/src/api/real/auth-real-api.ts b/frontend/src/api/real/auth-real-api.ts new file mode 100644 index 0000000..443506e --- /dev/null +++ b/frontend/src/api/real/auth-real-api.ts @@ -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 }); + } +}; diff --git a/frontend/src/assets/placeholder.svg b/frontend/src/assets/placeholder.svg new file mode 100644 index 0000000..5b97735 --- /dev/null +++ b/frontend/src/assets/placeholder.svg @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts index 076602b..f1ecc27 100644 --- a/frontend/src/hooks/useAuth.ts +++ b/frontend/src/hooks/useAuth.ts @@ -4,17 +4,11 @@ import { useEffect, useState } from "react"; import { handleServerError } from "@/utils/handle-server-error"; import { ApiError } from "@/errors/api-error"; -import { - DashboardService, - LoginService, - ShopLoginAccessTokenData, - UserPublic, - UserRegister, - UserService, - UserUpdate -} from "@/client"; +import { ShopLoginAccessTokenData, UserPublic } from "@/client"; import { toast } from "./useToast"; +import { authAPI } from "@/api/api"; + const isLoggedIn = () => { return localStorage.getItem("access_token") !== null; }; @@ -24,28 +18,22 @@ const useAuth = () => { const [loggedIn, setLoggedIn] = useState(isLoggedIn()); const navigate = useNavigate(); const queryClient = useQueryClient(); - + const { data: user } = useQuery({ queryKey: ["currentUser"], - queryFn: DashboardService.userGetUser, + queryFn: authAPI.getCurrentUser, enabled: loggedIn }); const signUpMutation = useMutation({ - mutationFn: (data: UserRegister) => - DashboardService.userRegister({ requestBody: data }), + mutationFn: authAPI.registerUser, onSuccess: () => navigate({ to: "/sign-in" }), onError: (err: ApiError) => handleServerError(err), onSettled: () => queryClient.invalidateQueries({ queryKey: ["users"] }) }); const login = async (data: ShopLoginAccessTokenData) => { - const response = await LoginService.dashboardLoginAccessToken({ - formData: { - username: data.formData.username, - password: data.formData.password - } - }); + const response = await authAPI.loginUser(data); localStorage.setItem("access_token", response.access_token); setLoggedIn(true); await queryClient.invalidateQueries({ queryKey: ["currentUser"] }); @@ -65,8 +53,7 @@ const useAuth = () => { }; const updateAccountMutation = useMutation({ - mutationFn: (data: UserUpdate) => - UserService.userUpdateUser({ requestBody: data }), + mutationFn: authAPI.updateUser, onSuccess: () => { toast({ title: "Account updated successfully" }); queryClient.invalidateQueries({ queryKey: ["currentUser"] }); @@ -77,8 +64,9 @@ const useAuth = () => { useEffect(() => { console.log("Checking whether the token is valid"); 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) { console.warn("User data is null while logged in, logging out."); logout(); diff --git a/frontend/src/hooks/useProduct.ts b/frontend/src/hooks/useProduct.ts new file mode 100644 index 0000000..84f0ee7 --- /dev/null +++ b/frontend/src/hooks/useProduct.ts @@ -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({ + 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) => 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 }; +} diff --git a/frontend/src/hooks/useProducts.ts b/frontend/src/hooks/useProducts.ts new file mode 100644 index 0000000..fd63ee4 --- /dev/null +++ b/frontend/src/hooks/useProducts.ts @@ -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({ + queryKey: ["products"], + queryFn: productsAPI.getProductsForShop + }); + + return query; +} diff --git a/frontend/src/hooks/useShop.ts b/frontend/src/hooks/useShop.ts index e69de29..fba1cd9 100644 --- a/frontend/src/hooks/useShop.ts +++ b/frontend/src/hooks/useShop.ts @@ -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({ + 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 + }; +} diff --git a/frontend/src/pages/products/components/product-dialog.tsx b/frontend/src/pages/products/components/product-dialog.tsx new file mode 100644 index 0000000..48ed144 --- /dev/null +++ b/frontend/src/pages/products/components/product-dialog.tsx @@ -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; + onSubmit: (e?: React.BaseSyntheticEvent) => void; + onDelete?: () => void; + onImageUpload: (e: React.ChangeEvent) => void; + submitLabel: string; + imagePreview?: string; + showDeleteButton?: boolean; +} + +export default function ProductDialog({ + open, + onOpenChange, + form, + onSubmit, + onDelete, + onImageUpload, + submitLabel, + imagePreview, + showDeleteButton = false +}: ProductDialogProps) { + return ( + + + + + {submitLabel} Product + + + +
+ {/* Main fields grid */} +
+ {/* Name field */} +
+ + + {form.formState.errors.name && ( +

+ {form.formState.errors.name.message} +

+ )} + +
+ + +
+ +
+ + +
+
+ + {/* Image upload & preview */} +
+ + {imagePreview ? ( + Preview + ) : ( + + + )} +

+ JPG or PNG only. Max size: 2MB. +

+ +
+ + {/* Description spanning full width */} +
+ +