From 104053f80238229c68124d9a51bf3fa9bc798309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afra=20=E2=80=8C?= Date: Wed, 17 Jun 2026 14:02:48 +0330 Subject: [PATCH 01/11] feat: landing page implemented --- eslint.config.js | 4 +- index.html | 2 +- package.json | 12 +- pnpm-lock.yaml | 433 ++++++++++++++++++++++++++-- src/assets/map.svg | 5 + src/assets/task.svg | 8 + src/components/ErrorBoundary.tsx | 5 +- src/context/LocationContext.tsx | 62 ++-- src/context/LocationContextValue.ts | 13 + src/hooks/useUserLocation.ts | 2 +- src/index.css | 113 +------- src/pages/Home.tsx | 115 +++++++- vite.config.js | 3 +- 13 files changed, 599 insertions(+), 178 deletions(-) create mode 100644 src/assets/map.svg create mode 100644 src/assets/task.svg create mode 100644 src/context/LocationContextValue.ts diff --git a/eslint.config.js b/eslint.config.js index ea36dd3..edabb8b 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2,14 +2,16 @@ import js from '@eslint/js' import globals from 'globals' import reactHooks from 'eslint-plugin-react-hooks' import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' import { defineConfig, globalIgnores } from 'eslint/config' export default defineConfig([ globalIgnores(['dist']), { - files: ['**/*.{js,jsx}'], + files: ['**/*.{js,jsx,ts,tsx}'], extends: [ js.configs.recommended, + ...tseslint.configs.recommended, reactHooks.configs.flat.recommended, reactRefresh.configs.vite, ], diff --git a/index.html b/index.html index 388c1b2..056788c 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,5 @@ - + diff --git a/package.json b/package.json index e90b082..d0ee15d 100644 --- a/package.json +++ b/package.json @@ -11,25 +11,27 @@ "preview": "vite preview" }, "dependencies": { + "@tanstack/react-query": "^5.9.0", "axios": "^1.6.6", "react": "^19.2.6", - "react-dom": "^19.2.6", - "@tanstack/react-query": "^5.9.0" + "react-dom": "^19.2.6" }, "devDependencies": { "@eslint/js": "^10.0.1", + "@tailwindcss/vite": "^4.3.1", "@types/node": "^20.5.1", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", "autoprefixer": "^10.4.14", - "postcss": "^8.4.26", - "tailwindcss": "^4.3.1", - "typescript": "^5.6.2", "eslint": "^10.3.0", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.6.0", + "postcss": "^8.4.26", + "tailwindcss": "^4.3.1", + "typescript": "^5.6.2", + "typescript-eslint": "^8.61.1", "vite": "^8.0.12" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08195d7..cebedaf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,7 +23,10 @@ importers: devDependencies: '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.5.0) + version: 10.0.1(eslint@10.5.0(jiti@2.7.0)) + '@tailwindcss/vite': + specifier: ^4.3.1 + version: 4.3.1(vite@8.0.16(@types/node@20.19.43)(jiti@2.7.0)) '@types/node': specifier: ^20.5.1 version: 20.19.43 @@ -35,19 +38,19 @@ importers: version: 19.2.3(@types/react@19.2.17) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.2(vite@8.0.16(@types/node@20.19.43)) + version: 6.0.2(vite@8.0.16(@types/node@20.19.43)(jiti@2.7.0)) autoprefixer: specifier: ^10.4.14 version: 10.5.0(postcss@8.5.15) eslint: specifier: ^10.3.0 - version: 10.5.0 + version: 10.5.0(jiti@2.7.0) eslint-plugin-react-hooks: specifier: ^7.1.1 - version: 7.1.1(eslint@10.5.0) + version: 7.1.1(eslint@10.5.0(jiti@2.7.0)) eslint-plugin-react-refresh: specifier: ^0.5.2 - version: 0.5.2(eslint@10.5.0) + version: 0.5.2(eslint@10.5.0(jiti@2.7.0)) globals: specifier: ^17.6.0 version: 17.6.0 @@ -60,9 +63,12 @@ importers: typescript: specifier: ^5.6.2 version: 5.9.3 + typescript-eslint: + specifier: ^8.61.1 + version: 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@5.9.3) vite: specifier: ^8.0.12 - version: 8.0.16(@types/node@20.19.43) + version: 8.0.16(@types/node@20.19.43)(jiti@2.7.0) packages: @@ -324,6 +330,100 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@tailwindcss/node@4.3.1': + resolution: {integrity: sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==} + + '@tailwindcss/oxide-android-arm64@4.3.1': + resolution: {integrity: sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.1': + resolution: {integrity: sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.1': + resolution: {integrity: sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.1': + resolution: {integrity: sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1': + resolution: {integrity: sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.1': + resolution: {integrity: sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.1': + resolution: {integrity: sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.1': + resolution: {integrity: sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.1': + resolution: {integrity: sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.1': + resolution: {integrity: sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.1': + resolution: {integrity: sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.1': + resolution: {integrity: sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.1': + resolution: {integrity: sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.1': + resolution: {integrity: sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + '@tanstack/query-core@5.101.0': resolution: {integrity: sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==} @@ -355,6 +455,65 @@ packages: '@types/react@19.2.17': resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@typescript-eslint/eslint-plugin@8.61.1': + resolution: {integrity: sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.61.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.61.1': + resolution: {integrity: sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.61.1': + resolution: {integrity: sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.61.1': + resolution: {integrity: sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.61.1': + resolution: {integrity: sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.61.1': + resolution: {integrity: sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.61.1': + resolution: {integrity: sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.61.1': + resolution: {integrity: sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.61.1': + resolution: {integrity: sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.61.1': + resolution: {integrity: sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitejs/plugin-react@6.0.2': resolution: {integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -464,6 +623,10 @@ packages: electron-to-chromium@1.5.372: resolution: {integrity: sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==} + enhanced-resolve@5.21.6: + resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==} + engines: {node: '>=10.13.0'} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -622,6 +785,9 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -648,6 +814,10 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -663,6 +833,10 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -773,6 +947,9 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -871,6 +1048,11 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + semver@7.8.4: + resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} + engines: {node: '>=10'} + hasBin: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -886,10 +1068,20 @@ packages: tailwindcss@4.3.1: resolution: {integrity: sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==} + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -897,6 +1089,13 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + typescript-eslint@8.61.1: + resolution: {integrity: sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -1100,9 +1299,9 @@ snapshots: tslib: 2.8.1 optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@10.5.0)': + '@eslint-community/eslint-utils@4.9.1(eslint@10.5.0(jiti@2.7.0))': dependencies: - eslint: 10.5.0 + eslint: 10.5.0(jiti@2.7.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -1123,9 +1322,9 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@10.0.1(eslint@10.5.0)': + '@eslint/js@10.0.1(eslint@10.5.0(jiti@2.7.0))': optionalDependencies: - eslint: 10.5.0 + eslint: 10.5.0(jiti@2.7.0) '@eslint/object-schema@3.0.5': {} @@ -1229,6 +1428,74 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@tailwindcss/node@4.3.1': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.21.6 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.1 + + '@tailwindcss/oxide-android-arm64@4.3.1': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.1': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.1': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.1': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.1': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.1': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.1': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.1': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.1': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.1': + optional: true + + '@tailwindcss/oxide@4.3.1': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.1 + '@tailwindcss/oxide-darwin-arm64': 4.3.1 + '@tailwindcss/oxide-darwin-x64': 4.3.1 + '@tailwindcss/oxide-freebsd-x64': 4.3.1 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.1 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.1 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.1 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.1 + '@tailwindcss/oxide-linux-x64-musl': 4.3.1 + '@tailwindcss/oxide-wasm32-wasi': 4.3.1 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.1 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.1 + + '@tailwindcss/vite@4.3.1(vite@8.0.16(@types/node@20.19.43)(jiti@2.7.0))': + dependencies: + '@tailwindcss/node': 4.3.1 + '@tailwindcss/oxide': 4.3.1 + tailwindcss: 4.3.1 + vite: 8.0.16(@types/node@20.19.43)(jiti@2.7.0) + '@tanstack/query-core@5.101.0': {} '@tanstack/react-query@5.101.0(react@19.2.7)': @@ -1259,10 +1526,101 @@ snapshots: dependencies: csstype: 3.2.3 - '@vitejs/plugin-react@6.0.2(vite@8.0.16(@types/node@20.19.43))': + '@typescript-eslint/eslint-plugin@8.61.1(@typescript-eslint/parser@8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.5.0(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/type-utils': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.61.1 + eslint: 10.5.0(jiti@2.7.0) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.61.1 + debug: 4.4.3 + eslint: 10.5.0(jiti@2.7.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.61.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.61.1(typescript@5.9.3) + '@typescript-eslint/types': 8.61.1 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.61.1': + dependencies: + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/visitor-keys': 8.61.1 + + '@typescript-eslint/tsconfig-utils@8.61.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@5.9.3) + debug: 4.4.3 + eslint: 10.5.0(jiti@2.7.0) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.61.1': {} + + '@typescript-eslint/typescript-estree@8.61.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.61.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.61.1(typescript@5.9.3) + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/visitor-keys': 8.61.1 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.4 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.5.0(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.61.1 + '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3) + eslint: 10.5.0(jiti@2.7.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.61.1': + dependencies: + '@typescript-eslint/types': 8.61.1 + eslint-visitor-keys: 5.0.1 + + '@vitejs/plugin-react@6.0.2(vite@8.0.16(@types/node@20.19.43)(jiti@2.7.0))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.0.16(@types/node@20.19.43) + vite: 8.0.16(@types/node@20.19.43)(jiti@2.7.0) acorn-jsx@5.3.2(acorn@8.17.0): dependencies: @@ -1359,6 +1717,11 @@ snapshots: electron-to-chromium@1.5.372: {} + enhanced-resolve@5.21.6: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -1378,20 +1741,20 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-plugin-react-hooks@7.1.1(eslint@10.5.0): + eslint-plugin-react-hooks@7.1.1(eslint@10.5.0(jiti@2.7.0)): dependencies: '@babel/core': 7.29.7 '@babel/parser': 7.29.7 - eslint: 10.5.0 + eslint: 10.5.0(jiti@2.7.0) hermes-parser: 0.25.1 zod: 4.4.3 zod-validation-error: 4.0.2(zod@4.4.3) transitivePeerDependencies: - supports-color - eslint-plugin-react-refresh@0.5.2(eslint@10.5.0): + eslint-plugin-react-refresh@0.5.2(eslint@10.5.0(jiti@2.7.0)): dependencies: - eslint: 10.5.0 + eslint: 10.5.0(jiti@2.7.0) eslint-scope@9.1.2: dependencies: @@ -1404,9 +1767,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.5.0: + eslint@10.5.0(jiti@2.7.0): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.5.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.5.0(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5 '@eslint/config-helpers': 0.6.0 @@ -1436,6 +1799,8 @@ snapshots: minimatch: 10.2.5 natural-compare: 1.4.0 optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 transitivePeerDependencies: - supports-color @@ -1528,6 +1893,8 @@ snapshots: gopd@1.2.0: {} + graceful-fs@4.2.11: {} + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -1553,6 +1920,8 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + imurmurhash@0.1.4: {} is-extglob@2.1.1: {} @@ -1563,6 +1932,8 @@ snapshots: isexe@2.0.0: {} + jiti@2.7.0: {} + js-tokens@4.0.0: {} jsesc@3.1.0: {} @@ -1641,6 +2012,10 @@ snapshots: dependencies: yallist: 3.1.1 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + math-intrinsics@1.1.0: {} mime-db@1.52.0: {} @@ -1732,6 +2107,8 @@ snapshots: semver@6.3.1: {} + semver@7.8.4: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -1742,11 +2119,17 @@ snapshots: tailwindcss@4.3.1: {} + tapable@2.3.3: {} + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + tslib@2.8.1: optional: true @@ -1754,6 +2137,17 @@ snapshots: dependencies: prelude-ls: 1.2.1 + typescript-eslint@8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.61.1(@typescript-eslint/parser@8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.5.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@5.9.3) + eslint: 10.5.0(jiti@2.7.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + typescript@5.9.3: {} undici-types@6.21.0: {} @@ -1768,7 +2162,7 @@ snapshots: dependencies: punycode: 2.3.1 - vite@8.0.16(@types/node@20.19.43): + vite@8.0.16(@types/node@20.19.43)(jiti@2.7.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -1778,6 +2172,7 @@ snapshots: optionalDependencies: '@types/node': 20.19.43 fsevents: 2.3.3 + jiti: 2.7.0 which@2.0.2: dependencies: diff --git a/src/assets/map.svg b/src/assets/map.svg new file mode 100644 index 0000000..b6a5d1a --- /dev/null +++ b/src/assets/map.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/task.svg b/src/assets/task.svg new file mode 100644 index 0000000..2538416 --- /dev/null +++ b/src/assets/task.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index acbb16c..a38aeb6 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -2,8 +2,8 @@ import React from 'react' type State = { hasError: boolean; error?: Error } -export default class ErrorBoundary extends React.Component, State> { - constructor(props: {}) { +export default class ErrorBoundary extends React.Component { + constructor(props: React.PropsWithChildren) { super(props) this.state = { hasError: false } } @@ -14,7 +14,6 @@ export default class ErrorBoundary extends React.Component void; -}; - -export const LocationContext = createContext(undefined); +import { LocationContext, LocationError } from "./LocationContextValue"; export const LocationProvider = ({ children }: { children: ReactNode }) => { const [location, setLocation] = useState(null); @@ -27,8 +17,6 @@ export const LocationProvider = ({ children }: { children: ReactNode }) => { setLocation(loc); setError(null); setLoading(false); - // Log once received - // eslint-disable-next-line no-console console.info("User location obtained", loc); }; @@ -37,11 +25,9 @@ export const LocationProvider = ({ children }: { children: ReactNode }) => { setLoading(false); if (err.code === err.PERMISSION_DENIED) { setError("permission_denied"); - // eslint-disable-next-line no-console console.warn("Geolocation permission denied", err.message); } else { setError("location_error"); - // eslint-disable-next-line no-console console.warn("Geolocation error", err.message); } }; @@ -74,26 +60,29 @@ export const LocationProvider = ({ children }: { children: ReactNode }) => { const requestLocation = useCallback(() => { // If Permissions API exists, read the state first to provide immediate feedback - if ((navigator as any).permissions && typeof (navigator as any).permissions.query === 'function') { + if (navigator.permissions && typeof navigator.permissions.query === "function") { try { - ;(navigator as any).permissions.query({ name: 'geolocation' }).then((status: PermissionStatus) => { - if (status.state === 'granted') { - // permission already granted + navigator.permissions + .query({ name: "geolocation" }) + .then((status) => { + if (status.state === "granted") { + // permission already granted + performGetPosition(); + } else if (status.state === "denied") { + // permission denied + setLoading(false); + setError("permission_denied"); + setLocation(null); + } else { + // prompt + performGetPosition(); + } + }) + .catch(() => { + // fallback to direct request performGetPosition(); - } else if (status.state === 'denied') { - // permission denied - setLoading(false); - setError('permission_denied'); - setLocation(null); - } else { - // prompt - performGetPosition(); - } - }).catch(() => { - // fallback to direct request - performGetPosition(); - }); - } catch (e) { + }); + } catch { performGetPosition(); } } else { @@ -103,9 +92,8 @@ export const LocationProvider = ({ children }: { children: ReactNode }) => { // Automatically request location on provider mount useEffect(() => { - requestLocation(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + queueMicrotask(requestLocation); + }, [requestLocation]); return ( diff --git a/src/context/LocationContextValue.ts b/src/context/LocationContextValue.ts new file mode 100644 index 0000000..f4d892f --- /dev/null +++ b/src/context/LocationContextValue.ts @@ -0,0 +1,13 @@ +import { createContext } from "react"; +import { Location } from "../types/location"; + +export type LocationError = "permission_denied" | "location_error" | null; + +export type LocationContextValue = { + location: Location | null; + loading: boolean; + error: LocationError; + requestLocation: () => void; +}; + +export const LocationContext = createContext(undefined); diff --git a/src/hooks/useUserLocation.ts b/src/hooks/useUserLocation.ts index 7db3960..ef1fbc7 100644 --- a/src/hooks/useUserLocation.ts +++ b/src/hooks/useUserLocation.ts @@ -1,5 +1,5 @@ import { useContext } from 'react' -import { LocationContext } from '../context/LocationContext' +import { LocationContext } from '../context/LocationContextValue' export function useUserLocation() { const ctx = useContext(LocationContext) diff --git a/src/index.css b/src/index.css index 2c84af0..4a553a5 100644 --- a/src/index.css +++ b/src/index.css @@ -1,111 +1,18 @@ -:root { - --text: #6b6375; - --text-h: #08060d; - --bg: #fff; - --border: #e5e4e7; - --code-bg: #f4f3ec; - --accent: #aa3bff; - --accent-bg: rgba(170, 59, 255, 0.1); - --accent-border: rgba(170, 59, 255, 0.5); - --social-bg: rgba(244, 243, 236, 0.5); - --shadow: - rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; +@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;500;600;700&display=swap'); +@import 'tailwindcss'; - --sans: system-ui, 'Segoe UI', Roboto, sans-serif; - --heading: system-ui, 'Segoe UI', Roboto, sans-serif; - --mono: ui-monospace, Consolas, monospace; - - font: 18px/145% var(--sans); - letter-spacing: 0.18px; - color-scheme: light dark; - color: var(--text); - background: var(--bg); - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - @media (max-width: 1024px) { - font-size: 16px; - } +@theme { + --font-sans: 'Vazirmatn', system-ui, 'Segoe UI', Roboto, sans-serif; } -@media (prefers-color-scheme: dark) { - :root { - --text: #9ca3af; - --text-h: #f3f4f6; - --bg: #16171d; - --border: #2e303a; - --code-bg: #1f2028; - --accent: #c084fc; - --accent-bg: rgba(192, 132, 252, 0.15); - --accent-border: rgba(192, 132, 252, 0.5); - --social-bg: rgba(47, 48, 58, 0.5); - --shadow: - rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; - } - - #social .button-icon { - filter: invert(1) brightness(2); - } +html, +body, +#root { + height: 100%; } body { margin: 0; -} - -#root { - width: 1126px; - max-width: 100%; - margin: 0 auto; - text-align: center; - border-inline: 1px solid var(--border); - min-height: 100svh; - display: flex; - flex-direction: column; - box-sizing: border-box; -} - -h1, -h2 { - font-family: var(--heading); - font-weight: 500; - color: var(--text-h); -} - -h1 { - font-size: 56px; - letter-spacing: -1.68px; - margin: 32px 0; - @media (max-width: 1024px) { - font-size: 36px; - margin: 20px 0; - } -} -h2 { - font-size: 24px; - line-height: 118%; - letter-spacing: -0.24px; - margin: 0 0 8px; - @media (max-width: 1024px) { - font-size: 20px; - } -} -p { - margin: 0; -} - -code, -.counter { - font-family: var(--mono); - display: inline-flex; - border-radius: 4px; - color: var(--text-h); -} - -code { - font-size: 15px; - line-height: 135%; - padding: 4px 8px; - background: var(--code-bg); + font-family: var(--font-sans); + color-scheme: light; } diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index adee61d..9f8ae19 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,12 +1,113 @@ -import React from 'react' -import LocationStatus from '../components/LocationStatus' +import { useState } from 'react' -export default function Home() { +type Cafe = { + id: number + name: string + seats: number + image: string | null +} + +const mockCafes: Cafe[] = [ + { id: 1, name: 'کافه بی‌بای', seats: 8, image: null }, + { id: 2, name: 'کافه بی‌بای', seats: 10, image: null }, + { id: 3, name: 'کافه ستاره', seats: 15, image: null }, +] + +function ChevronDownIcon() { return ( -
-
- -
+ + + + ) +} + +function MapPlaceholder() { + return ( +
+ نقشه +
+ ) +} + +function SelectorRow({ label, value }: { label: string; value: string }) { + return ( +
+

{label}

+ +
+ ) +} + +function CafeCard({ cafe }: { cafe: Cafe }) { + return ( +
+
+

{cafe.name}

+ {cafe.seats} صندلی +
+
+
+ ) +} + +export default function Home() { + const [view, setView] = useState<'list' | 'map'>('list') + + return ( +
+ + + + + +
+

کافه‌ها

+
+ + +
+
+ +
+ {view === 'list' ? ( +
+ {mockCafes.map((cafe) => ( + + ))} +
+ ) : ( + + )} +
+ +
) } diff --git a/vite.config.js b/vite.config.js index 8b0f57b..c4069b7 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,7 +1,8 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' // https://vite.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [react(), tailwindcss()], }) -- 2.45.3 From 75f40df7e08e468eb538af4a8fee1e7ad217bf25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afra=20=E2=80=8C?= Date: Sat, 20 Jun 2026 12:17:44 +0330 Subject: [PATCH 02/11] feat: customer reservation flow --- package.json | 6 +- pnpm-lock.yaml | 98 ++++++++++ src/App.tsx | 24 ++- src/assets/call.svg | 3 + src/assets/info.svg | 3 + src/assets/location.svg | 4 + src/assets/minus.svg | 1 + src/assets/plus.svg | 3 + src/components/InfoIcon.tsx | 15 ++ src/components/ReservationModal.tsx | 211 +++++++++++++++++++++ src/data/mockCafes.ts | 55 ++++++ src/pages/CafeDetail.tsx | 272 ++++++++++++++++++++++++++++ src/pages/Home.tsx | 127 ++++++++++--- src/utils/formatNumber.ts | 3 + 14 files changed, 796 insertions(+), 29 deletions(-) create mode 100644 src/assets/call.svg create mode 100644 src/assets/info.svg create mode 100644 src/assets/location.svg create mode 100644 src/assets/minus.svg create mode 100644 src/assets/plus.svg create mode 100644 src/components/InfoIcon.tsx create mode 100644 src/components/ReservationModal.tsx create mode 100644 src/data/mockCafes.ts create mode 100644 src/pages/CafeDetail.tsx create mode 100644 src/utils/formatNumber.ts diff --git a/package.json b/package.json index d0ee15d..ee7f152 100644 --- a/package.json +++ b/package.json @@ -13,12 +13,16 @@ "dependencies": { "@tanstack/react-query": "^5.9.0", "axios": "^1.6.6", + "leaflet": "^1.9.4", "react": "^19.2.6", - "react-dom": "^19.2.6" + "react-dom": "^19.2.6", + "react-leaflet": "^5.0.0", + "react-router-dom": "^7.17.0" }, "devDependencies": { "@eslint/js": "^10.0.1", "@tailwindcss/vite": "^4.3.1", + "@types/leaflet": "^1.9.21", "@types/node": "^20.5.1", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cebedaf..3f41338 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,12 +14,21 @@ importers: axios: specifier: ^1.6.6 version: 1.17.0 + leaflet: + specifier: ^1.9.4 + version: 1.9.4 react: specifier: ^19.2.6 version: 19.2.7 react-dom: specifier: ^19.2.6 version: 19.2.7(react@19.2.7) + react-leaflet: + specifier: ^5.0.0 + version: 5.0.0(leaflet@1.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react-router-dom: + specifier: ^7.17.0 + version: 7.17.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) devDependencies: '@eslint/js': specifier: ^10.0.1 @@ -27,6 +36,9 @@ importers: '@tailwindcss/vite': specifier: ^4.3.1 version: 4.3.1(vite@8.0.16(@types/node@20.19.43)(jiti@2.7.0)) + '@types/leaflet': + specifier: ^1.9.21 + version: 1.9.21 '@types/node': specifier: ^20.5.1 version: 20.19.43 @@ -232,6 +244,13 @@ packages: '@oxc-project/types@0.133.0': resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + '@react-leaflet/core@3.0.0': + resolution: {integrity: sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==} + peerDependencies: + leaflet: ^1.9.0 + react: ^19.0.0 + react-dom: ^19.0.0 + '@rolldown/binding-android-arm64@1.0.3': resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -441,9 +460,15 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/leaflet@1.9.21': + resolution: {integrity: sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==} + '@types/node@20.19.43': resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} @@ -589,6 +614,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -862,6 +891,9 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + leaflet@1.9.4: + resolution: {integrity: sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -1032,6 +1064,30 @@ packages: peerDependencies: react: ^19.2.7 + react-leaflet@5.0.0: + resolution: {integrity: sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==} + peerDependencies: + leaflet: ^1.9.0 + react: ^19.0.0 + react-dom: ^19.0.0 + + react-router-dom@7.17.0: + resolution: {integrity: sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.17.0: + resolution: {integrity: sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + react@19.2.7: resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} engines: {node: '>=0.10.0'} @@ -1053,6 +1109,9 @@ packages: engines: {node: '>=10'} hasBin: true + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1377,6 +1436,12 @@ snapshots: '@oxc-project/types@0.133.0': {} + '@react-leaflet/core@3.0.0(leaflet@1.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + leaflet: 1.9.4 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + '@rolldown/binding-android-arm64@1.0.3': optional: true @@ -1512,8 +1577,14 @@ snapshots: '@types/estree@1.0.9': {} + '@types/geojson@7946.0.16': {} + '@types/json-schema@7.0.15': {} + '@types/leaflet@1.9.21': + dependencies: + '@types/geojson': 7946.0.16 + '@types/node@20.19.43': dependencies: undici-types: 6.21.0 @@ -1691,6 +1762,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie@1.1.1: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -1950,6 +2023,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + leaflet@1.9.4: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -2080,6 +2155,27 @@ snapshots: react: 19.2.7 scheduler: 0.27.0 + react-leaflet@5.0.0(leaflet@1.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + '@react-leaflet/core': 3.0.0(leaflet@1.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + leaflet: 1.9.4 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + react-router-dom@7.17.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-router: 7.17.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + + react-router@7.17.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + cookie: 1.1.1 + react: 19.2.7 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.7(react@19.2.7) + react@19.2.7: {} rolldown@1.0.3: @@ -2109,6 +2205,8 @@ snapshots: semver@7.8.4: {} + set-cookie-parser@2.7.2: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 diff --git a/src/App.tsx b/src/App.tsx index ae18f15..c98b041 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,26 @@ -import React from 'react' +import { useEffect } from 'react' +import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom' import Home from './pages/Home' +import CafeDetail from './pages/CafeDetail' + +function ScrollToTop() { + const { pathname } = useLocation() + + useEffect(() => { + window.scrollTo(0, 0) + }, [pathname]) + + return null +} export default function App() { - return + return ( + + + + } /> + } /> + + + ) } diff --git a/src/assets/call.svg b/src/assets/call.svg new file mode 100644 index 0000000..693c4bc --- /dev/null +++ b/src/assets/call.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/info.svg b/src/assets/info.svg new file mode 100644 index 0000000..1b66977 --- /dev/null +++ b/src/assets/info.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/location.svg b/src/assets/location.svg new file mode 100644 index 0000000..462a973 --- /dev/null +++ b/src/assets/location.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/minus.svg b/src/assets/minus.svg new file mode 100644 index 0000000..e1d50eb --- /dev/null +++ b/src/assets/minus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/plus.svg b/src/assets/plus.svg new file mode 100644 index 0000000..fc2de2c --- /dev/null +++ b/src/assets/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/InfoIcon.tsx b/src/components/InfoIcon.tsx new file mode 100644 index 0000000..fa2bd6c --- /dev/null +++ b/src/components/InfoIcon.tsx @@ -0,0 +1,15 @@ +export default function InfoIcon({ className = 'h-5 w-5 shrink-0 text-gray-400' }: { className?: string }) { + return ( + + + + ) +} diff --git a/src/components/ReservationModal.tsx b/src/components/ReservationModal.tsx new file mode 100644 index 0000000..0994263 --- /dev/null +++ b/src/components/ReservationModal.tsx @@ -0,0 +1,211 @@ +import { useEffect, useState } from "react"; +import { createPortal } from "react-dom"; +import { Cafe } from "../data/mockCafes"; +import { formatPersianNumber } from "../utils/formatNumber"; +import InfoIcon from "./InfoIcon"; + +interface ReservationModalProps { + isOpen: boolean; + onClose: () => void; + cafe: Cafe; +} + +const mockPhone = "۰۹۱۲۳۴۵۶۷۸۹"; +// mock wallet balance — will come from the user's wallet once auth is implemented +const walletBalance = 10000; + +function MinusIcon({ className = "" }: { className?: string }) { + return ( + + + + ); +} + +function PlusIcon({ className = "" }: { className?: string }) { + return ( + + + + ); +} + +function PaymentRow({ + label, + amount, + emphasize = false, + bottomBorder = false, + infoIconParam = false, +}: { + label: string; + amount: number; + emphasize?: boolean; + bottomBorder?: boolean; + infoIconParam?: boolean; +}) { + return ( +
+
+ + {label} + + {infoIconParam && ( + + )} +
+ + {formatPersianNumber(amount)} تومان + +
+ ); +} + +export default function ReservationModal({ + isOpen, + onClose, + cafe, +}: ReservationModalProps) { + const [fullName, setFullName] = useState(""); + const [count, setCount] = useState(1); + + useEffect(() => { + if (!isOpen) return; + + const previousOverflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; + return () => { + document.body.style.overflow = previousOverflow; + }; + }, [isOpen]); + + if (!isOpen) return null; + + const reservationAmount = cafe.entryFeeAmount * count; + const payableAmount = Math.max(0, reservationAmount - walletBalance); + + return createPortal( + <> +
+
+
+ +
+

+ {cafe.name} +

+

+ {cafe.match} +

+ +
+ + setFullName(e.target.value)} + placeholder="نام و نام خانوادگی" + className="w-full rounded-2xl border border-gray-200 px-4 py-3 text-right" + /> +
+ +
+ + {/* readOnly mock for now — once OTP auth ships, this comes from the auth context */} + +
+ +
+ +
+ + + {formatPersianNumber(count)} نفر + + +
+
+ +

+ پرداخت +

+ + + + +
+ +

+ توجه داشته باشید که مسئولیت رزرو و حضور به‌موقع در کافه بر عهده + کاربر است. +

+
+
+ +
+ +
+
+ , + document.body, + ); +} diff --git a/src/data/mockCafes.ts b/src/data/mockCafes.ts new file mode 100644 index 0000000..6f720da --- /dev/null +++ b/src/data/mockCafes.ts @@ -0,0 +1,55 @@ +export interface Cafe { + id: number + name: string + match: string + entryFee: string + entryFeeAmount: number + remainingCapacity: number + tags: string[] + location: { lat: number; lng: number } + phone: string + address: string + images: string[] // empty array for now, will show gray placeholder +} + +export const mockCafes: Cafe[] = [ + { + id: 1, + name: 'کافه بی‌بای', + match: 'بازی برزیل-مراکش (پنجشنبه ۲۷ خرداد - ساعت ۲:۰۰)', + entryFee: '۲,۰۰۰,۰۰۰ تومان', + entryFeeAmount: 2000000, + remainingCapacity: 3, + tags: ['پارکینگ دارد', 'فضای سیگار', 'امکان سفارش'], + location: { lat: 35.7219, lng: 51.3347 }, + phone: '۰۲۱۹۱۰۲۰۲۱۲۰۲', + address: 'ونک، خدامی، نبش نیروی انتظامی، پلاک ۲', + images: [], + }, + { + id: 2, + name: 'کافه بی‌بای شعبه ۲', + match: 'بازی برزیل-مراکش (پنجشنبه ۲۷ خرداد - ساعت ۲:۰۰)', + entryFee: '۱,۵۰۰,۰۰۰ تومان', + entryFeeAmount: 1500000, + remainingCapacity: 10, + tags: ['فضای سیگار', 'امکان سفارش'], + location: { lat: 35.755, lng: 51.4165 }, + phone: '۰۲۱۸۸۴۴۵۵۶۶', + address: 'جردن، خیابان آفریقا، پلاک ۱۲', + images: [], + }, + { + id: 3, + name: 'کافه ستاره', + match: 'بازی برزیل-مراکش (پنجشنبه ۲۷ خرداد - ساعت ۲:۰۰)', + entryFee: 'رایگان', + entryFeeAmount: 0, + remainingCapacity: 15, + tags: ['پارکینگ دارد'], + location: { lat: 35.6892, lng: 51.389 }, + phone: '۰۲۱۷۷۳۳۲۲۱۱', + address: 'انقلاب، خیابان ۱۶ آذر، پلاک ۵', + images: [], + }, +] diff --git a/src/pages/CafeDetail.tsx b/src/pages/CafeDetail.tsx new file mode 100644 index 0000000..4a0b445 --- /dev/null +++ b/src/pages/CafeDetail.tsx @@ -0,0 +1,272 @@ +import { ReactNode, useState } from "react"; +import { useNavigate, useParams, useLocation } from "react-router-dom"; +import { MapContainer, TileLayer, Marker, ZoomControl } from "react-leaflet"; +import "leaflet/dist/leaflet.css"; +import L from "leaflet"; +import markerIcon2x from "leaflet/dist/images/marker-icon-2x.png"; +import markerIcon from "leaflet/dist/images/marker-icon.png"; +import markerShadow from "leaflet/dist/images/marker-shadow.png"; +import { mockCafes, Cafe } from "../data/mockCafes"; +import InfoIcon from "../components/InfoIcon"; +import ReservationModal from "../components/ReservationModal"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +delete (L.Icon.Default.prototype as any)._getIconUrl; +L.Icon.Default.mergeOptions({ + iconUrl: markerIcon, + iconRetinaUrl: markerIcon2x, + shadowUrl: markerShadow, +}); + +function ChevronLeftIcon() { + return ( + + + + ); +} + +function ChevronRightIcon() { + return ( + + + + ); +} + +function PhoneIcon() { + return ( + + + + ); +} + +function MapPinIcon() { + return ( + + + + + ); +} + +function Pill({ + children, + className = "bg-gray-100 text-gray-700", +}: { + children: ReactNode; + className?: string; +}) { + return ( + + {children} + + ); +} + +function ImageCarousel({ images }: { images: string[] }) { + const [index, setIndex] = useState(0); + const slideCount = Math.max(images.length, 1); + + return ( +
+ + +
+ {Array.from({ length: slideCount }).map((_, i) => ( + + ))} +
+
+ ); +} + +function toEnglishDigits(value: string) { + const persianDigits = "۰۱۲۳۴۵۶۷۸۹"; + return value.replace(/[۰-۹]/g, (digit) => String(persianDigits.indexOf(digit))); +} + +async function openNavigationPicker(cafe: Cafe) { + const mapsUrl = `https://www.google.com/maps/dir/?api=1&destination=${cafe.location.lat},${cafe.location.lng}`; + + if (navigator.share) { + try { + await navigator.share({ title: cafe.name, url: mapsUrl }); + } catch { + // user dismissed the native share sheet + } + } else { + window.open(mapsUrl, "_blank"); + } +} + +export default function CafeDetail() { + const { id } = useParams(); + const location = useLocation(); + const navigate = useNavigate(); + const [isReservationOpen, setIsReservationOpen] = useState(false); + + const cafe: Cafe | undefined = + (location.state as { cafe?: Cafe } | null)?.cafe ?? + mockCafes.find((c) => c.id === Number(id)); + + if (!cafe) { + return ( +
+ کافه پیدا نشد +
+ ); + } + + return ( +
+
+ + +
+ +
+

+ {cafe.name} +

+

{cafe.match}

+ +
+ ورودی: نفری {cafe.entryFee} + ظرفیت باقی مانده: {cafe.remainingCapacity}نفر + {/*
*/} + + {/*
*/} + {cafe.tags.map((tag) => ( + + {tag} + + ))} +
+ +

+ آدرس +

+ + + + + + +
+
+ +
+

شماره تماس

+ + {cafe.phone} + +
+
+
+ +
+

آدرس

+ {cafe.address} +
+
+ + +
+ +
+ +

+ توجه داشته باشید که مسئولیت رزرو و حضور به‌موقع در کافه بر عهده + کاربر است. +

+
+
+ +
+ +
+ + setIsReservationOpen(false)} + cafe={cafe} + /> +
+ ); +} diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 9f8ae19..376a05d 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,70 +1,143 @@ import { useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { mockCafes, Cafe } from '../data/mockCafes' -type Cafe = { - id: number - name: string - seats: number - image: string | null -} - -const mockCafes: Cafe[] = [ - { id: 1, name: 'کافه بی‌بای', seats: 8, image: null }, - { id: 2, name: 'کافه بی‌بای', seats: 10, image: null }, - { id: 3, name: 'کافه ستاره', seats: 15, image: null }, +const cityOptions = ['تهران', 'اصفهان', 'مشهد'] +const matchOptions = [ + 'برزیل-مراکش (۳ روز دیگر-۲:۰۰)', + 'آرژانتین-فرانسه (۲ روز دیگر-۱۸:۰۰)', + 'اسپانیا-آلمان (۱ روز دیگر-۲۰:۳۰)', ] -function ChevronDownIcon() { +function ChevronDownIcon({ className = '' }: { className?: string }) { return ( - + ) } +function MapIcon() { + return ( + + + + + + ) +} + +function ListIcon() { + return ( + + + + + + + + + ) +} + function MapPlaceholder() { return (
- نقشه + بنر
) } -function SelectorRow({ label, value }: { label: string; value: string }) { +function SelectorRow({ + label, + value, + options, + onChange, +}: { + label: string + value: string + options: string[] + onChange: (value: string) => void +}) { + const [open, setOpen] = useState(false) + return ( -
+

{label}

+ + {open && ( + <> +
setOpen(false)} /> +
+ {options.map((option) => ( + + ))} +
+ + )}
) } function CafeCard({ cafe }: { cafe: Cafe }) { + const navigate = useNavigate() + return ( -
+ ) } export default function Home() { const [view, setView] = useState<'list' | 'map'>('list') + const [city, setCity] = useState(cityOptions[0]) + const [match, setMatch] = useState(matchOptions[0]) return (
- - + +

کافه‌ها

@@ -73,18 +146,20 @@ export default function Home() { type="button" onClick={() => setView('map')} className={`flex items-center gap-1 rounded-full px-3 py-1.5 text-sm ${ - view === 'map' ? 'bg-[#1e2d4a] text-white' : 'text-gray-600' + view === 'map' ? 'bg-[#223D5D] text-white' : 'text-gray-600' }`} > + نقشه
@@ -104,7 +179,7 @@ export default function Home() { diff --git a/src/utils/formatNumber.ts b/src/utils/formatNumber.ts new file mode 100644 index 0000000..cdb04c2 --- /dev/null +++ b/src/utils/formatNumber.ts @@ -0,0 +1,3 @@ +export function formatPersianNumber(n: number): string { + return n.toLocaleString('fa-IR') +} -- 2.45.3 From cf26ec0b133d47480cd61064426fb907e4b94579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afra=20=E2=80=8C?= Date: Sat, 20 Jun 2026 13:06:55 +0330 Subject: [PATCH 03/11] feat: customer reservation flow; fallback success --- src/App.tsx | 2 + src/assets/success_fallback.svg | 4 ++ src/pages/PaymentSuccess.tsx | 75 +++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 src/assets/success_fallback.svg create mode 100644 src/pages/PaymentSuccess.tsx diff --git a/src/App.tsx b/src/App.tsx index c98b041..997d872 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import { useEffect } from 'react' import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom' import Home from './pages/Home' import CafeDetail from './pages/CafeDetail' +import PaymentSuccess from './pages/PaymentSuccess' function ScrollToTop() { const { pathname } = useLocation() @@ -20,6 +21,7 @@ export default function App() { } /> } /> + } /> ) diff --git a/src/assets/success_fallback.svg b/src/assets/success_fallback.svg new file mode 100644 index 0000000..4528e14 --- /dev/null +++ b/src/assets/success_fallback.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pages/PaymentSuccess.tsx b/src/pages/PaymentSuccess.tsx new file mode 100644 index 0000000..1f16916 --- /dev/null +++ b/src/pages/PaymentSuccess.tsx @@ -0,0 +1,75 @@ +import { useNavigate } from 'react-router-dom' +import InfoIcon from '../components/InfoIcon' + +function SuccessIcon() { + return ( + + + + + ) +} + +function DetailRow({ label, value }: { label: string; value: string }) { + return ( +
+ {label} + {value} +
+ ) +} + +// mock reservation summary — will come from the payment gateway's return params once integrated +const mockReservation = { + count: '۳ نفر', + fullName: 'هستی حصادی', + phone: '۰۹۱۲۳۴۵۶۷۸۹', + cafeName: 'بی‌بای', + matchName: 'برزیل-مراکش', + dateTime: 'پنجشنبه ۲۷ خرداد - ساعت ۲:۰۰', + reservationCode: 'HH-۲۸۴۷', +} + +export default function PaymentSuccess() { + const navigate = useNavigate() + + return ( +
+
+ + +

جزئیات رزرو

+
+ + + + + + + +
+ +
+ +

+ کد رزرو را برای ورود نزد خود نگه دارید. اطلاعات رزرو و کد ورود تا دقایقی دیگر برای شما پیامک می‌شود. +

+
+ + +
+
+ ) +} -- 2.45.3 From c2382b23f1ce531c22dad1bdc379dd432ea73c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afra=20=E2=80=8C?= Date: Sat, 20 Jun 2026 15:29:17 +0330 Subject: [PATCH 04/11] feat: cafe owner registeration --- src/App.tsx | 2 + src/assets/arrow-right.svg | 3 + src/components/ReservationModal.tsx | 4 +- src/data/mockCafeFeatures.ts | 12 ++ src/pages/CafeOwnerRegister.tsx | 282 ++++++++++++++++++++++++++++ 5 files changed, 301 insertions(+), 2 deletions(-) create mode 100644 src/assets/arrow-right.svg create mode 100644 src/data/mockCafeFeatures.ts create mode 100644 src/pages/CafeOwnerRegister.tsx diff --git a/src/App.tsx b/src/App.tsx index 997d872..48f2aaa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@ import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom' import Home from './pages/Home' import CafeDetail from './pages/CafeDetail' import PaymentSuccess from './pages/PaymentSuccess' +import CafeOwnerRegister from './pages/CafeOwnerRegister' function ScrollToTop() { const { pathname } = useLocation() @@ -22,6 +23,7 @@ export default function App() { } /> } /> } /> + } /> ) diff --git a/src/assets/arrow-right.svg b/src/assets/arrow-right.svg new file mode 100644 index 0000000..2694710 --- /dev/null +++ b/src/assets/arrow-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/ReservationModal.tsx b/src/components/ReservationModal.tsx index 0994263..57f5890 100644 --- a/src/components/ReservationModal.tsx +++ b/src/components/ReservationModal.tsx @@ -117,7 +117,7 @@ export default function ReservationModal({ value={fullName} onChange={(e) => setFullName(e.target.value)} placeholder="نام و نام خانوادگی" - className="w-full rounded-2xl border border-gray-200 px-4 py-3 text-right" + className="w-full rounded-2xl border border-gray-200 px-4 py-3 text-right placeholder:font-light placeholder:text-[#999999]" />
@@ -131,7 +131,7 @@ export default function ReservationModal({ value={mockPhone} readOnly placeholder="مثال: ۰۹۱۲۳۴۵۶۷۸۹" - className="w-full rounded-2xl border border-gray-200 bg-gray-50 px-4 py-3 text-right" + className="w-full rounded-2xl border border-gray-200 bg-gray-50 px-4 py-3 text-right placeholder:font-light placeholder:text-[#999999]" />
diff --git a/src/data/mockCafeFeatures.ts b/src/data/mockCafeFeatures.ts new file mode 100644 index 0000000..8171d11 --- /dev/null +++ b/src/data/mockCafeFeatures.ts @@ -0,0 +1,12 @@ +export interface CafeFeature { + id: string + label: string +} + +export const mockCafeFeatures: CafeFeature[] = [ + { id: 'open-space', label: 'فضای باز' }, + { id: 'family-friendly', label: 'فضای مناسب خانواده' }, + { id: 'smoking-allowed', label: 'امکان کشیدن سیگار' }, + { id: 'parking', label: 'پارکینگ' }, + { id: 'delivery', label: 'امکان سفارش' }, +] diff --git a/src/pages/CafeOwnerRegister.tsx b/src/pages/CafeOwnerRegister.tsx new file mode 100644 index 0000000..37d7a22 --- /dev/null +++ b/src/pages/CafeOwnerRegister.tsx @@ -0,0 +1,282 @@ +import { ReactNode, useState } from "react"; +import InfoIcon from "../components/InfoIcon"; +import { mockCafeFeatures } from "../data/mockCafeFeatures"; + +function ArrowIcon({ className = "" }: { className?: string }) { + return ( + + + + ); +} + +function CheckIcon() { + return ( + + + + ); +} + +function StepCircle({ + active, + children, +}: { + active: boolean; + children: ReactNode; +}) { + return ( + + {children} + + ); +} + +function Stepper({ currentStep }: { currentStep: 1 | 2 }) { + return ( +
+ + اطلاعات نماینده + + ۱ + + ۲ + + اطلاعات کافه + +
+ ); +} + +function FormField({ + label, + placeholder, + value, + onChange, +}: { + label: string; + placeholder: string; + value: string; + onChange: (value: string) => void; +}) { + return ( +
+ + onChange(e.target.value)} + placeholder={placeholder} + className="w-full rounded-2xl border border-gray-200 px-4 py-3 text-right h-14 placeholder:font-light placeholder:text-[#999999]" + /> +
+ ); +} + +function UploadBox({ + label, + buttonLabel, +}: { + label: string; + buttonLabel: string; +}) { + return ( +
+ + +
+ ); +} + +function FeatureChip({ + label, + checked, + onToggle, +}: { + label: string; + checked: boolean; + onToggle: () => void; +}) { + return ( + + ); +} + +export default function CafeOwnerRegister() { + const [step, setStep] = useState<1 | 2>(1); + + const [fullName, setFullName] = useState(""); + const [phone, setPhone] = useState(""); + const [nationalId, setNationalId] = useState(""); + const [cardNumber, setCardNumber] = useState(""); + + const [cafeName, setCafeName] = useState(""); + const [address, setAddress] = useState(""); + const [cafePhone, setCafePhone] = useState(""); + const [selectedFeatures, setSelectedFeatures] = useState([]); + + const toggleFeature = (feature: string) => { + setSelectedFeatures((prev) => + prev.includes(feature) + ? prev.filter((f) => f !== feature) + : [...prev, feature], + ); + }; + + return ( +
+
+ + + +
+ {step === 1 ? ( + <> +
+ +

+ لطفاً یک{" "} + نماینده{" "} + برای کافه معرفی کنید. نماینده باید در اپلیکیشن ثبت‌نام کند و + پرداختها و واریز به کیف پول یا واریز می‌شود. شماره موبایل، کد + ملی و کارت بانکی باید متعلق به یک نفر باشند. +

+
+ + + + + + + ) : ( + <> + + + + + + + +
+ +
+ {mockCafeFeatures.map((feature) => ( + toggleFeature(feature.id)} + /> + ))} +
+
+ + )} +
+ +
+ + +
+
+ ); +} -- 2.45.3 From d02a96cce019e903e17d92959cbdfa48aa0a6c35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afra=20=E2=80=8C?= Date: Sun, 21 Jun 2026 11:17:05 +0330 Subject: [PATCH 05/11] feat: cafe panel completed --- src/App.tsx | 10 ++ src/assets/book.svg | 6 + src/assets/calendar.svg | 12 ++ src/assets/profile-circle.svg | 5 + src/assets/search-normal.svg | 4 + src/components/SuccessIcon.tsx | 14 ++ src/data/mockCafeEvents.ts | 11 ++ src/data/mockReservations.ts | 13 ++ src/pages/AddEvent.tsx | 226 ++++++++++++++++++++++++++++++ src/pages/AddEventSuccess.tsx | 34 +++++ src/pages/CafeOwnerPanel.tsx | 110 +++++++++++++++ src/pages/CafeOwnerRegister.tsx | 16 ++- src/pages/CafeRegisterSuccess.tsx | 38 +++++ src/pages/EventReservations.tsx | 211 ++++++++++++++++++++++++++++ src/pages/PaymentSuccess.tsx | 16 +-- 15 files changed, 706 insertions(+), 20 deletions(-) create mode 100644 src/assets/book.svg create mode 100644 src/assets/calendar.svg create mode 100644 src/assets/profile-circle.svg create mode 100644 src/assets/search-normal.svg create mode 100644 src/components/SuccessIcon.tsx create mode 100644 src/data/mockCafeEvents.ts create mode 100644 src/data/mockReservations.ts create mode 100644 src/pages/AddEvent.tsx create mode 100644 src/pages/AddEventSuccess.tsx create mode 100644 src/pages/CafeOwnerPanel.tsx create mode 100644 src/pages/CafeRegisterSuccess.tsx create mode 100644 src/pages/EventReservations.tsx diff --git a/src/App.tsx b/src/App.tsx index 48f2aaa..7429c85 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,11 @@ import Home from './pages/Home' import CafeDetail from './pages/CafeDetail' import PaymentSuccess from './pages/PaymentSuccess' import CafeOwnerRegister from './pages/CafeOwnerRegister' +import CafeRegisterSuccess from './pages/CafeRegisterSuccess' +import CafeOwnerPanel from './pages/CafeOwnerPanel' +import AddEvent from './pages/AddEvent' +import AddEventSuccess from './pages/AddEventSuccess' +import EventReservations from './pages/EventReservations' function ScrollToTop() { const { pathname } = useLocation() @@ -24,6 +29,11 @@ export default function App() { } /> } /> } /> + } /> + } /> + } /> + } /> + } /> ) diff --git a/src/assets/book.svg b/src/assets/book.svg new file mode 100644 index 0000000..a95cb42 --- /dev/null +++ b/src/assets/book.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/calendar.svg b/src/assets/calendar.svg new file mode 100644 index 0000000..ef1c748 --- /dev/null +++ b/src/assets/calendar.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/profile-circle.svg b/src/assets/profile-circle.svg new file mode 100644 index 0000000..8f1f86a --- /dev/null +++ b/src/assets/profile-circle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/search-normal.svg b/src/assets/search-normal.svg new file mode 100644 index 0000000..c0c4ec3 --- /dev/null +++ b/src/assets/search-normal.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/SuccessIcon.tsx b/src/components/SuccessIcon.tsx new file mode 100644 index 0000000..98fecf7 --- /dev/null +++ b/src/components/SuccessIcon.tsx @@ -0,0 +1,14 @@ +export default function SuccessIcon({ className = 'mx-auto h-28 w-28' }: { className?: string }) { + return ( + + + + + ) +} diff --git a/src/data/mockCafeEvents.ts b/src/data/mockCafeEvents.ts new file mode 100644 index 0000000..62862c7 --- /dev/null +++ b/src/data/mockCafeEvents.ts @@ -0,0 +1,11 @@ +export interface CafeEvent { + id: number + matchLabel: string + remainingSeats: number + totalSeats: number +} + +export const mockCafeEvents: CafeEvent[] = [ + { id: 1, matchLabel: 'بازی برزیل-مراکش (۳ روز دیگر-۲:۰۰)', remainingSeats: 8, totalSeats: 8 }, + { id: 2, matchLabel: 'بازی برزیل-مراکش (۳ روز دیگر-۲:۰۰)', remainingSeats: 0, totalSeats: 8 }, +] diff --git a/src/data/mockReservations.ts b/src/data/mockReservations.ts new file mode 100644 index 0000000..6e17337 --- /dev/null +++ b/src/data/mockReservations.ts @@ -0,0 +1,13 @@ +export interface Reservation { + id: number + code: string + count: number + name: string + phone: string +} + +export const mockReservations: Reservation[] = [ + { id: 1, code: 'QC-۸۸۸۷', count: 3, name: 'وینسو وینسوزاده', phone: '۰۹۱۲۳۴۵۶۷۸۹' }, + { id: 2, code: 'QC-۸۸۸۷', count: 3, name: 'وینسو وینسوزاده', phone: '۰۹۱۲۳۴۵۶۷۸۹' }, + { id: 3, code: 'QC-۸۸۸۷', count: 3, name: 'وینسو وینسوزاده', phone: '۰۹۱۲۳۴۵۶۷۸۹' }, +] diff --git a/src/pages/AddEvent.tsx b/src/pages/AddEvent.tsx new file mode 100644 index 0000000..d7e5c8e --- /dev/null +++ b/src/pages/AddEvent.tsx @@ -0,0 +1,226 @@ +import { useEffect, useRef, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { formatPersianNumber } from '../utils/formatNumber' + +const matchOptions = [ + 'برزیل-مراکش (۳ روز دیگر-۲:۰۰)', + 'آرژانتین-فرانسه (۲ روز دیگر-۱۸:۰۰)', + 'اسپانیا-آلمان (۱ روز دیگر-۲۰:۳۰)', +] + +const ENTRY_FEE_STEP = 10000 +const DESCRIPTION_MAX_LENGTH = 200 + +function toEnglishDigits(value: string) { + const persianDigits = '۰۱۲۳۴۵۶۷۸۹' + return value.replace(/[۰-۹]/g, (digit) => String(persianDigits.indexOf(digit))) +} + +function ChevronIcon({ className = '' }: { className?: string }) { + return ( + + + + ) +} + +function ChevronDownIcon({ className = '' }: { className?: string }) { + return ( + + + + ) +} + +function MinusIcon({ className = '' }: { className?: string }) { + return ( + + + + ) +} + +function PlusIcon({ className = '' }: { className?: string }) { + return ( + + + + ) +} + +function MatchSelect({ value, onChange }: { value: string; onChange: (value: string) => void }) { + const [open, setOpen] = useState(false) + + return ( +
+ + + + {open && ( + <> +
setOpen(false)} /> +
+ {matchOptions.map((option) => ( + + ))} +
+ + )} +
+ ) +} + +function Stepper({ + value, + step, + min = 0, + suffix, + editable = false, + onChange, +}: { + value: number + step: number + min?: number + suffix: string + editable?: boolean + onChange: (next: number) => void +}) { + const atMin = value <= min + const measureRef = useRef(null) + const [inputWidth, setInputWidth] = useState(50) + + useEffect(() => { + if (editable && measureRef.current) { + setInputWidth(measureRef.current.offsetWidth) + } + }, [editable, value]) + + const handleInputChange = (raw: string) => { + const digitsOnly = toEnglishDigits(raw).replace(/[^\d]/g, '') + onChange(digitsOnly === '' ? min : Math.max(min, Number(digitsOnly))) + } + + return ( +
+ + + {editable ? ( +
+ + {formatPersianNumber(value)} + + handleInputChange(e.target.value)} + style={inputWidth ? { width: `${inputWidth}px` } : undefined} + className="bg-transparent text-left font-medium text-gray-900 focus:outline-none" + /> + {suffix} +
+ ) : ( + + {formatPersianNumber(value)} {suffix} + + )} + + +
+ ) +} + +function DescriptionField({ value, onChange }: { value: string; onChange: (value: string) => void }) { + return ( +
+ +
+