diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..988ada0 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +VITE_API_BASE_URL=https://reservation-staging.winsoo.ir/api/ +VITE_AUTH_BASE_URL=https://accounts-staging.gooyal.ir +VITE_AUTH_CLIENT_ID= +VITE_AUTH_CLIENT_SECRET= +VITE_APP_BASE_URL= diff --git a/.env.staging b/.env.staging new file mode 100644 index 0000000..b04b165 --- /dev/null +++ b/.env.staging @@ -0,0 +1,2 @@ +VITE_APP_BASE_URL=https://reservation-staging.winsoo.ir/ +VITE_API_BASE_URL=https://reservation-staging.winsoo.ir/api/ diff --git a/.gitignore b/.gitignore index b7b9ab0..e0e39f0 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ dist .pnpm-store dist-ssr *.local +.env # Editor directories and files .vscode/* 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..5b8b69e 100644 --- a/package.json +++ b/package.json @@ -6,30 +6,37 @@ "scripts": { "dev": "vite", "build": "vite build", + "build:staging": "vite build --mode staging", "lint": "eslint .", "typecheck": "tsc --noEmit", "preview": "vite preview" }, "dependencies": { + "@tanstack/react-query": "^5.9.0", "axios": "^1.6.6", + "leaflet": "^1.9.4", "react": "^19.2.6", "react-dom": "^19.2.6", - "@tanstack/react-query": "^5.9.0" + "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", "@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..3f41338 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,16 +14,31 @@ 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 - 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/leaflet': + specifier: ^1.9.21 + version: 1.9.21 '@types/node': specifier: ^20.5.1 version: 20.19.43 @@ -35,19 +50,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 +75,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: @@ -226,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} @@ -324,6 +349,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==} @@ -341,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==} @@ -355,6 +480,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} @@ -430,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'} @@ -464,6 +652,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 +814,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 +843,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 +862,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==} @@ -688,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'} @@ -773,6 +979,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'} @@ -855,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'} @@ -871,6 +1104,14 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + semver@7.8.4: + resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} + 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'} @@ -886,10 +1127,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 +1148,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 +1358,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 +1381,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': {} @@ -1178,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 @@ -1229,6 +1493,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)': @@ -1245,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 @@ -1259,10 +1597,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: @@ -1333,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 @@ -1359,6 +1790,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 +1814,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 +1840,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 +1872,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 +1966,8 @@ snapshots: gopd@1.2.0: {} + graceful-fs@4.2.11: {} + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -1553,6 +1993,8 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + imurmurhash@0.1.4: {} is-extglob@2.1.1: {} @@ -1563,6 +2005,8 @@ snapshots: isexe@2.0.0: {} + jiti@2.7.0: {} + js-tokens@4.0.0: {} jsesc@3.1.0: {} @@ -1579,6 +2023,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + leaflet@1.9.4: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -1641,6 +2087,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: {} @@ -1705,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: @@ -1732,6 +2203,10 @@ snapshots: semver@6.3.1: {} + semver@7.8.4: {} + + set-cookie-parser@2.7.2: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -1742,11 +2217,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 +2235,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 +2260,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 +2270,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/App.tsx b/src/App.tsx index ae18f15..8421a84 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,48 @@ -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' +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' +import MyReservations from './pages/MyReservations' +import Login from './pages/Login' +import LoginOtp from './pages/LoginOtp' +import RequireRole from './components/RequireRole' + +function ScrollToTop() { + const { pathname } = useLocation() + + useEffect(() => { + window.scrollTo(0, 0) + }, [pathname]) + + return null +} export default function App() { - return + return ( + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ) } 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/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/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/edit-2.svg b/src/assets/edit-2.svg new file mode 100644 index 0000000..5ee713b --- /dev/null +++ b/src/assets/edit-2.svg @@ -0,0 +1,5 @@ + + + + + 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/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/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/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/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/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/BookIcon.tsx b/src/components/BookIcon.tsx new file mode 100644 index 0000000..0115d55 --- /dev/null +++ b/src/components/BookIcon.tsx @@ -0,0 +1,10 @@ +export default function BookIcon() { + return ( + + + + + + + ) +} 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 + + + ) +} diff --git a/src/components/RequireRole.tsx b/src/components/RequireRole.tsx new file mode 100644 index 0000000..1bae174 --- /dev/null +++ b/src/components/RequireRole.tsx @@ -0,0 +1,11 @@ +import { ReactNode } from 'react' +import { Navigate } from 'react-router-dom' +import { getUserRole, UserRole } from '../utils/role' + +export default function RequireRole({ role, children }: { role: UserRole; children: ReactNode }) { + const userRole = getUserRole() + if (userRole !== role) { + return + } + return <>{children} +} diff --git a/src/components/ReservationModal.tsx b/src/components/ReservationModal.tsx new file mode 100644 index 0000000..57f5890 --- /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 placeholder:font-light placeholder:text-[#999999]" + /> +
+ +
+ + {/* readOnly mock for now — once OTP auth ships, this comes from the auth context */} + +
+ +
+ +
+ + + {formatPersianNumber(count)} نفر + + +
+
+ +

+ پرداخت +

+ + + + +
+ +

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

+
+
+ +
+ +
+
+ , + document.body, + ); +} 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/context/LocationContext.tsx b/src/context/LocationContext.tsx index b870093..c68ffce 100644 --- a/src/context/LocationContext.tsx +++ b/src/context/LocationContext.tsx @@ -1,16 +1,6 @@ -import React, { createContext, useCallback, useEffect, useRef, useState, ReactNode } from "react"; +import { useCallback, useEffect, useRef, useState, ReactNode } from "react"; import { Location } from "../types/location"; - -type LocationError = "permission_denied" | "location_error" | null; - -type LocationContextValue = { - location: Location | null; - loading: boolean; - error: LocationError; - requestLocation: () => 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/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/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/data/mockMyReservations.ts b/src/data/mockMyReservations.ts new file mode 100644 index 0000000..d1b5a63 --- /dev/null +++ b/src/data/mockMyReservations.ts @@ -0,0 +1,33 @@ +export interface MyReservation { + id: number + matchLabel: string + dateTimeLabel: string + count: number + code: string + name: string + phone: string + cafeName: string +} + +export const mockMyReservations: MyReservation[] = [ + { + id: 1, + matchLabel: 'برزیل-مراکش', + dateTimeLabel: 'سه‌شنبه ۲۰ خرداد - ساعت ۰۰:۳۰', + count: 3, + code: 'QC-۸۸۸۷', + name: 'وینسو وینسوزاده', + phone: '۰۹۱۲۳۴۵۶۷۸۹', + cafeName: 'بی‌بای', + }, + { + id: 2, + matchLabel: 'برزیل-مراکش', + dateTimeLabel: 'سه‌شنبه ۲۰ خرداد - ساعت ۰۰:۳۰', + count: 3, + code: 'QC-۸۸۸۷', + name: 'وینسو وینسوزاده', + phone: '۰۹۱۲۳۴۵۶۷۸۹', + cafeName: 'بی‌بای', + }, +] 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/hooks/useMatchEvents.ts b/src/hooks/useMatchEvents.ts new file mode 100644 index 0000000..377d795 --- /dev/null +++ b/src/hooks/useMatchEvents.ts @@ -0,0 +1,19 @@ +import { useEffect, useState } from 'react' +import { fetchMatchEvents, MatchEvent } from '../services/campaignApi' +import { useNow } from './useNow' + +export function useMatchEvents() { + const [matches, setMatches] = useState([]) + const now = useNow() + + useEffect(() => { + fetchMatchEvents() + .then(setMatches) + .catch(() => {}) + }, []) + + // drop matches whose kickoff has already passed — only upcoming ones should ever be selectable + const upcomingMatches = matches.filter((match) => new Date(match.match_datetime).getTime() > now.getTime()) + + return { matches: upcomingMatches, now } +} diff --git a/src/hooks/useNow.ts b/src/hooks/useNow.ts new file mode 100644 index 0000000..86e5db1 --- /dev/null +++ b/src/hooks/useNow.ts @@ -0,0 +1,12 @@ +import { useEffect, useState } from 'react' + +export function useNow() { + const [now, setNow] = useState(() => new Date()) + + useEffect(() => { + const timer = setInterval(() => setNow(new Date()), 1000) + return () => clearInterval(timer) + }, []) + + return now +} 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/AddEvent.tsx b/src/pages/AddEvent.tsx new file mode 100644 index 0000000..6201158 --- /dev/null +++ b/src/pages/AddEvent.tsx @@ -0,0 +1,261 @@ +import { useEffect, useRef, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { formatPersianNumber } from '../utils/formatNumber' +import { createCafeSchedule, MatchEvent } from '../services/campaignApi' +import { useMatchEvents } from '../hooks/useMatchEvents' +import { formatMatchLabel } from '../utils/matchCountdown' +import { getCafeUuid } from '../utils/cafe' + +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, + options, + now, + onChange, +}: { + value: string + options: MatchEvent[] + now: Date + onChange: (uuid: string) => void +}) { + const [open, setOpen] = useState(false) + const selectedMatch = options.find((match) => match.uuid === value) + + return ( +
+ + + + {open && ( + <> +
setOpen(false)} /> +
+ {options.map((match) => ( + + ))} +
+ + )} +
+ ) +} + +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 ( +
+ +
+