Compare commits

..

77 Commits

Author SHA1 Message Date
hamid zarghami 83c8c3a121 box ai
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-29 12:00:53 +03:30
hamid zarghami 3d2de6fd4b problem solved
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-29 09:41:41 +03:30
hamid zarghami 2248845467 show ai link in header
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-29 09:41:09 +03:30
hamid zarghami b19c1cd3ca navigate back in ai [age
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-28 09:32:30 +03:30
hamid zarghami 2465ae1020 fix
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-26 15:50:30 +03:30
hamid zarghami 0cb119629c badge and show ai button
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-26 14:40:33 +03:30
hamid zarghami da42e42b7c base url
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-26 11:39:05 +03:30
hamid zarghami 6849062f28 offer food
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-26 10:37:59 +03:30
hamid zarghami 3409b8114e base url
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-23 11:16:24 +03:30
hamid zarghami 4c32f06da7 merge
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-23 11:14:36 +03:30
hamid zarghami a9fc7c4ff7 compeleted ai 2026-07-23 11:14:07 +03:30
hamid zarghami e10177a788 ui ai page 2026-07-23 10:03:37 +03:30
morteza 07ea4a961b add prepare time
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-19 16:14:23 +03:30
morteza 234729329c review
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-17 10:54:14 +03:30
hamid zarghami 81fc074708 pull
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-15 12:44:37 +03:30
hamid zarghami f2172e537d review list 2026-07-15 12:43:39 +03:30
hamid zarghami f8bddfb428 public review 2026-07-15 12:30:13 +03:30
morteza 29ad49185b disable cancell button
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-14 10:13:36 +03:30
hamid zarghami cb2a0e85c4 sefid barfi
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-09 12:30:29 +03:30
hamid zarghami 9e70565748 update cache
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-08 10:37:41 +03:30
hamid zarghami db15454a83 all restaurant
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-05 11:20:56 +03:30
hamid zarghami 7ff5570c3f image in single food
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-04 15:37:07 +03:30
hamid zarghami 136acf1f4c slider 2026-07-04 10:15:30 +03:30
hamid zarghami 5921ac0bc7 last uupdate parralax
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-29 16:38:40 +03:30
hamid zarghami 30e6279cb2 better parralex
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-29 16:04:37 +03:30
hamid zarghami 1bfd61f496 zoom with scrol
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-29 15:28:02 +03:30
hamid zarghami c1f2851e25 logo image
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-29 14:24:30 +03:30
hamid zarghami 7c12b19511 multi card
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-29 12:19:44 +03:30
hamid zarghami a14be45b76 multi phones 2026-06-29 12:15:52 +03:30
hamid zarghami 6bdb3c80af parallax
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-29 09:52:20 +03:30
hamid zarghami 816b278283 badge cart
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-27 16:09:32 +03:30
hamid zarghami c6a2a54806 add about
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-27 15:46:27 +03:30
hamid zarghami 4bfebeab35 menuindex back
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-27 14:47:08 +03:30
hamid zarghami 1d0ffe6a11 change size with scroll
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-27 14:15:15 +03:30
hamid zarghami fae7da51fa bottom navbar like instagram 2026-06-27 12:43:52 +03:30
hamid zarghami bcfaceae94 base bottom navbar 2026-06-27 12:21:32 +03:30
hamid zarghami 5ea96647ad fix shadow
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-27 09:45:44 +03:30
hamid zarghami d592dd525e fix design bug category
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-23 16:08:17 +03:30
hamid zarghami 5f8812ce55 single page food
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-23 15:55:41 +03:30
hamid zarghami a53ffaf619 border
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-23 14:44:02 +03:30
hamid zarghami ecd43b0307 shadow fix in about
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-23 14:35:38 +03:30
hamid zarghami 62a255cb70 fix category active item 2026-06-23 14:32:32 +03:30
hamid zarghami 201bb768c9 disable food
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-23 14:16:41 +03:30
hamid zarghami 037e94d7ac remove province and city
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-23 14:12:02 +03:30
hamid zarghami cee1f98972 deactive category
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-23 09:24:09 +03:30
hamid zarghami a0b0266b1a menu bottom color 2026-06-23 09:17:51 +03:30
hamid zarghami c282edd0c6 shadow and don't availablity 2026-06-23 09:07:01 +03:30
hamid zarghami 8ff2d4adf3 fix default
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-22 15:00:53 +03:30
hamid zarghami 80acaf743c bgType 2026-06-22 12:18:49 +03:30
hamid zarghami bace34a20f update domain 2026-06-22 12:10:27 +03:30
hamid zarghami b2d4f7848f update domains 2026-06-22 10:05:35 +03:30
hamid zarghami e8a950b0b6 background image 2026-06-22 09:20:08 +03:30
hamid zarghami 3bb6ffac4e glass with just background 2026-06-21 15:46:04 +03:30
hamid zarghami ac049ef067 game glass 2026-06-21 15:39:28 +03:30
hamid zarghami 97b1cddf85 fix combo box in glass 2026-06-21 15:30:08 +03:30
hamid zarghami ed5df3d7b7 textarea cart 2026-06-21 15:08:14 +03:30
hamid zarghami c565c0957f fix problem 2026-06-21 15:04:52 +03:30
hamid zarghami d34a3a92f1 glass other page 2026-06-21 15:02:18 +03:30
hamid zarghami de45b1f197 Cache 2026-06-21 14:52:20 +03:30
hamid zarghami ba82744219 food item discount color 2026-06-21 14:29:49 +03:30
hamid zarghami b5653906dc glass mode 2026-06-21 12:58:29 +03:30
hamid zarghami d3ef8e1a98 load pattern 2026-06-21 11:08:11 +03:30
hamid zarghami f6cfa369fc update type about 2026-06-21 10:11:00 +03:30
hamid zarghami f002d3c658 liara registery
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-20 16:06:19 +03:30
hamid zarghami 68a290edfa Seyed pull
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-20 16:04:04 +03:30
hamid zarghami e350e22399 category box 2026-06-20 16:02:48 +03:30
morteza 78e4097d8c ltr input
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-18 23:09:35 +03:30
hamid zarghami 461112c8ab update docker
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-16 11:02:43 +03:30
hamid zarghami 4bf4d57a18 milco domain 2026-06-16 11:02:21 +03:30
morteza 5f8e95ee6a optimized docker
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-16 09:41:28 +03:30
hamid zarghami a3d64df54d update docker
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-16 09:34:58 +03:30
hamid zarghami 78d45e7280 remove sharp 2026-06-16 09:33:57 +03:30
morteza d95d3420ba fix type
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-16 09:32:58 +03:30
morteza fb694c0297 fix docker
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-16 09:24:48 +03:30
hamid zarghami 7a00b884ad fix build
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-16 09:01:50 +03:30
hamid zarghami fc8ac729f7 update design
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-16 08:55:31 +03:30
hamid zarghami 97e4d81b09 disable foods
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-06-15 17:05:01 +03:30
126 changed files with 5158 additions and 3023 deletions
+10 -1
View File
@@ -11,4 +11,13 @@ node_modules
coverage
npm-debug.log
.dockerignore
Dockerfile*
Dockerfile*
tsconfig.tsbuildinfo
next-env.d.ts
public/tower_game-master/src
public/tower_game-master/package.json
public/tower_game-master/package-lock.json
public/the-cube-master/src
public/the-cube-master/package.json
public/the-cube-master/package-lock.json
public/the-cube-master/rollup.config.*
+19 -22
View File
@@ -1,11 +1,13 @@
# syntax=docker/dockerfile:1
FROM node:20-alpine AS base
# Change Alpine repo
# RUN sed -i 's|https://dl-cdn.alpinelinux.org/alpine|https://mirror.de.velop.ir/alpine|g' /etc/apk/repositories
RUN sed -i 's|https://dl-cdn.alpinelinux.org/alpine|https://mirror.de.velop.ir/alpine|g' /etc/apk/repositories
# Configure npm registry mirror (Liara)
# ENV NPM_CONFIG_REGISTRY=https://mirror-npm.runflare.com
# RUN npm config set registry https://mirror-npm.runflare.com
ENV NPM_CONFIG_REGISTRY=https://package-mirror.liara.ir/repository/npm/
RUN npm config set registry https://package-mirror.liara.ir/repository/npm/
# Install tzdata to support timezone settings
# RUN apk add --no-cache tzdata && \
@@ -13,38 +15,33 @@ FROM node:20-alpine AS base
# echo "Asia/Tehran" > /etc/timezone
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1 \
npm_config_update_notifier=false
FROM base AS deps
# RUN apk add --no-cache libc6-compat git
COPY package*.json ./
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --legacy-peer-deps --ignore-scripts --loglevel info
npm ci --legacy-peer-deps --prefer-offline
FROM base AS builder
WORKDIR /build
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
ARG NEXT_PUBLIC_API_BASE_URL=https://api.danakcorp.com/
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
RUN --mount=type=cache,target=/app/.next/cache \
npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production \
PORT=3000 \
HOSTNAME=0.0.0.0
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
RUN mkdir .next && chown appuser:appgroup .next
COPY --from=builder --chown=appuser:appgroup /build/.next/standalone ./
COPY --from=builder --chown=appuser:appgroup /build/.next/static ./.next/static
COPY --from=builder --chown=appuser:appgroup /build/public ./public
COPY --from=builder --chown=appuser:appgroup /build/package.json ./package.json
COPY --from=builder --chown=appuser:appgroup /app/public ./public
COPY --from=builder --chown=appuser:appgroup /app/.next/standalone ./
COPY --from=builder --chown=appuser:appgroup /app/.next/static ./.next/static
USER appuser
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
ENV NEXT_PUBLIC_API_BASE_URL=https://api.danakcorp.com/
CMD ["node", "server.js"]
+1 -1
View File
@@ -30,7 +30,7 @@ const nextConfig: NextConfig = {
],
},
{
source: "/((?!sw\\.js|api/pwa-icon).*)",
source: "/((?!sw\\.js).*)",
headers: [
{
key: "Cache-Control",
+4 -1
View File
@@ -44,7 +44,6 @@
"react-leaflet": "^5.0.0",
"react-otp-input": "^3.1.1",
"react-virtualized": "^9.22.6",
"sharp": "^0.34.5",
"tailwind-merge": "^3.3.1",
"use-long-press": "^3.3.0",
"yup": "^1.7.1",
@@ -1860,6 +1859,7 @@
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
"integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=18"
}
@@ -5643,6 +5643,7 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"devOptional": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
@@ -9623,6 +9624,7 @@
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"devOptional": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -9692,6 +9694,7 @@
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"@img/colour": "^1.0.0",
"detect-libc": "^2.1.2",
-1
View File
@@ -45,7 +45,6 @@
"react-leaflet": "^5.0.0",
"react-otp-input": "^3.1.1",
"react-virtualized": "^9.22.6",
"sharp": "^0.34.5",
"tailwind-merge": "^3.3.1",
"use-long-press": "^3.3.0",
"yup": "^1.7.1",
Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

+101
View File
@@ -0,0 +1,101 @@
<svg width="299" height="97" viewBox="0 0 299 97" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M148.337 7.64759C148.337 11.8811 148.474 16.1145 148.337 20.348C148.2 23.489 146.147 25.8106 143.683 27.7225C143.273 27.9956 142.862 28.2687 142.178 28.815C143.41 29.6344 144.368 30.3172 145.463 31.1366H118.5C118.911 31 119.184 30.8634 119.321 30.8634C123.29 30.0441 127.533 29.7709 131.365 28.1322C140.399 24.3084 146.01 17.6168 148.2 8.05728L148.337 7.64759Z" fill="url(#paint0_linear_13943_20144)"/>
<path d="M152.169 23.3524C151.759 23.8987 151.211 24.4449 150.937 24.8546C150.527 25.4009 150.39 25.9471 149.979 26.63V0C149.979 0 149.979 0 150.116 0C150.527 2.04846 150.801 3.96035 151.211 6.00881C152.99 16.3877 158.739 23.7621 168.456 27.859C170.099 28.5419 172.015 28.9515 173.794 29.6344C173.657 29.6344 173.52 29.6344 173.384 29.6344C169.962 29.6344 166.677 29.6344 163.255 29.6344C158.739 29.7709 155.454 27.7225 152.854 24.0352C152.854 23.8987 152.58 23.6256 152.443 23.489C152.443 23.489 152.443 23.489 152.169 23.3524Z" fill="url(#paint1_linear_13943_20144)"/>
<path d="M126.986 32.3656C130.681 32.3656 134.514 32.3656 138.209 32.3656C141.768 32.3656 144.368 34.4141 146.421 37.1454C146.695 37.5551 146.969 37.9648 147.516 38.6476C148.337 37.5551 149.022 36.4626 149.843 35.5066V62C149.432 59.815 149.158 57.7665 148.748 55.5815C146.558 43.5639 139.578 35.9163 127.67 32.6388C127.533 32.6388 127.26 32.6388 127.123 32.5022C127.123 32.5022 127.123 32.5022 127.123 32.3656H126.986Z" fill="url(#paint2_linear_13943_20144)"/>
<path d="M151.347 53.8062C151.347 49.8458 151.211 45.8855 151.347 41.9251C151.484 38.6476 153.4 36.1894 156.001 34.2775C156.411 34.0044 156.822 33.7313 157.506 33.185C156.411 32.3656 155.317 31.6828 154.222 31H180.5C178.721 31.2731 177.078 31.6828 175.299 31.9559C168.866 32.9119 163.255 35.5066 158.738 40.1498C155.18 43.7004 152.853 47.9339 151.621 52.8502C151.621 53.1234 151.484 53.3965 151.347 53.8062Z" fill="url(#paint3_linear_13943_20144)"/>
<path d="M148.337 7.64759C148.337 11.8811 148.474 16.1145 148.337 20.348C148.2 23.489 146.147 25.8106 143.683 27.7225C143.273 27.9956 142.862 28.2687 142.178 28.815C143.41 29.6344 144.368 30.3172 145.463 31.1366H118.5C118.911 31 119.184 30.8634 119.321 30.8634C123.29 30.0441 127.533 29.7709 131.365 28.1322C140.399 24.3084 146.01 17.6168 148.2 8.05728L148.337 7.64759Z" fill="url(#paint4_linear_13943_20144)"/>
<path d="M152.169 23.3524C151.759 23.8987 151.211 24.4449 150.937 24.8546C150.527 25.4009 150.39 25.9471 149.979 26.63V0C149.979 0 149.979 0 150.116 0C150.527 2.04846 150.801 3.96035 151.211 6.00881C152.99 16.3877 158.739 23.7621 168.456 27.859C170.099 28.5419 172.015 28.9515 173.794 29.6344C173.657 29.6344 173.52 29.6344 173.384 29.6344C169.962 29.6344 166.677 29.6344 163.255 29.6344C158.739 29.7709 155.454 27.7225 152.854 24.0352C152.854 23.8987 152.58 23.6256 152.443 23.489C152.443 23.489 152.443 23.489 152.169 23.3524Z" fill="url(#paint5_linear_13943_20144)"/>
<path d="M126.986 32.3656C130.681 32.3656 134.514 32.3656 138.209 32.3656C141.768 32.3656 144.368 34.4141 146.421 37.1454C146.695 37.5551 146.969 37.9648 147.516 38.6476C148.337 37.5551 149.022 36.4626 149.843 35.5066V62C149.432 59.815 149.158 57.7665 148.748 55.5815C146.558 43.5639 139.578 35.9163 127.67 32.6388C127.533 32.6388 127.26 32.6388 127.123 32.5022C127.123 32.5022 127.123 32.5022 127.123 32.3656H126.986Z" fill="url(#paint6_linear_13943_20144)"/>
<path d="M151.347 53.8062C151.347 49.8458 151.211 45.8855 151.347 41.9251C151.484 38.6476 153.4 36.1894 156.001 34.2775C156.411 34.0044 156.822 33.7313 157.506 33.185C156.411 32.3656 155.317 31.6828 154.222 31H180.5C178.721 31.2731 177.078 31.6828 175.299 31.9559C168.866 32.9119 163.255 35.5066 158.738 40.1498C155.18 43.7004 152.853 47.9339 151.621 52.8502C151.621 53.1234 151.484 53.3965 151.347 53.8062Z" fill="url(#paint7_linear_13943_20144)"/>
<path d="M148.337 7.64759C148.337 11.8811 148.474 16.1145 148.337 20.348C148.2 23.489 146.147 25.8106 143.683 27.7225C143.273 27.9956 142.862 28.2687 142.178 28.815C143.41 29.6344 144.368 30.3172 145.463 31.1366H118.5C118.911 31 119.184 30.8634 119.321 30.8634C123.29 30.0441 127.533 29.7709 131.365 28.1322C140.399 24.3084 146.01 17.6168 148.2 8.05728L148.337 7.64759Z" fill="url(#paint8_linear_13943_20144)"/>
<path d="M152.169 23.3524C151.759 23.8987 151.211 24.4449 150.937 24.8546C150.527 25.4009 150.39 25.9471 149.979 26.63V0C149.979 0 149.979 0 150.116 0C150.527 2.04846 150.801 3.96035 151.211 6.00881C152.99 16.3877 158.739 23.7621 168.456 27.859C170.099 28.5419 172.015 28.9515 173.794 29.6344C173.657 29.6344 173.52 29.6344 173.384 29.6344C169.962 29.6344 166.677 29.6344 163.255 29.6344C158.739 29.7709 155.454 27.7225 152.854 24.0352C152.854 23.8987 152.58 23.6256 152.443 23.489C152.443 23.489 152.443 23.489 152.169 23.3524Z" fill="url(#paint9_linear_13943_20144)"/>
<path d="M126.986 32.3656C130.681 32.3656 134.514 32.3656 138.209 32.3656C141.768 32.3656 144.368 34.4141 146.421 37.1454C146.695 37.5551 146.969 37.9648 147.516 38.6476C148.337 37.5551 149.022 36.4626 149.843 35.5066V62C149.432 59.815 149.158 57.7665 148.748 55.5815C146.558 43.5639 139.578 35.9163 127.67 32.6388C127.533 32.6388 127.26 32.6388 127.123 32.5022C127.123 32.5022 127.123 32.5022 127.123 32.3656H126.986Z" fill="url(#paint10_linear_13943_20144)"/>
<path d="M151.347 53.8062C151.347 49.8458 151.211 45.8855 151.347 41.9251C151.484 38.6476 153.4 36.1894 156.001 34.2775C156.411 34.0044 156.822 33.7313 157.506 33.185C156.411 32.3656 155.317 31.6828 154.222 31H180.5C178.721 31.2731 177.078 31.6828 175.299 31.9559C168.866 32.9119 163.255 35.5066 158.738 40.1498C155.18 43.7004 152.853 47.9339 151.621 52.8502C151.621 53.1234 151.484 53.3965 151.347 53.8062Z" fill="url(#paint11_linear_13943_20144)"/>
<path d="M148.337 7.64759C148.337 11.8811 148.474 16.1145 148.337 20.348C148.2 23.489 146.147 25.8106 143.683 27.7225C143.273 27.9956 142.862 28.2687 142.178 28.815C143.41 29.6344 144.368 30.3172 145.463 31.1366H118.5C118.911 31 119.184 30.8634 119.321 30.8634C123.29 30.0441 127.533 29.7709 131.365 28.1322C140.399 24.3084 146.01 17.6168 148.2 8.05728L148.337 7.64759Z" fill="url(#paint12_linear_13943_20144)"/>
<path d="M152.169 23.3524C151.759 23.8987 151.211 24.4449 150.937 24.8546C150.527 25.4009 150.39 25.9471 149.979 26.63V0C149.979 0 149.979 0 150.116 0C150.527 2.04846 150.801 3.96035 151.211 6.00881C152.99 16.3877 158.739 23.7621 168.456 27.859C170.099 28.5419 172.015 28.9515 173.794 29.6344C173.657 29.6344 173.52 29.6344 173.384 29.6344C169.962 29.6344 166.677 29.6344 163.255 29.6344C158.739 29.7709 155.454 27.7225 152.854 24.0352C152.854 23.8987 152.58 23.6256 152.443 23.489C152.443 23.489 152.443 23.489 152.169 23.3524Z" fill="url(#paint13_linear_13943_20144)"/>
<path d="M126.986 32.3656C130.681 32.3656 134.514 32.3656 138.209 32.3656C141.768 32.3656 144.368 34.4141 146.421 37.1454C146.695 37.5551 146.969 37.9648 147.516 38.6476C148.337 37.5551 149.022 36.4626 149.843 35.5066V62C149.432 59.815 149.158 57.7665 148.748 55.5815C146.558 43.5639 139.578 35.9163 127.67 32.6388C127.533 32.6388 127.26 32.6388 127.123 32.5022C127.123 32.5022 127.123 32.5022 127.123 32.3656H126.986Z" fill="url(#paint14_linear_13943_20144)"/>
<path d="M151.347 53.8062C151.347 49.8458 151.211 45.8855 151.347 41.9251C151.484 38.6476 153.4 36.1894 156.001 34.2775C156.411 34.0044 156.822 33.7313 157.506 33.185C156.411 32.3656 155.317 31.6828 154.222 31H180.5C178.721 31.2731 177.078 31.6828 175.299 31.9559C168.866 32.9119 163.255 35.5066 158.738 40.1498C155.18 43.7004 152.853 47.9339 151.621 52.8502C151.621 53.1234 151.484 53.3965 151.347 53.8062Z" fill="url(#paint15_linear_13943_20144)"/>
<path d="M97.6982 83.8669V69H101.869C103.147 69 104.291 69.2691 105.3 69.8745C106.309 70.4127 107.049 71.2872 107.587 72.2963C108.125 73.3726 108.394 74.5835 108.394 75.9289V76.8707C108.394 78.2834 108.125 79.4943 107.587 80.5706C107.049 81.647 106.309 82.4542 105.3 82.9924C104.291 83.5305 103.147 83.8669 101.802 83.8669H97.6982ZM99.6491 70.6145V82.2524H101.735C103.214 82.2524 104.425 81.7815 105.233 80.8397C106.04 79.8979 106.511 78.5525 106.511 76.8034V75.9289C106.511 74.2471 106.107 72.9017 105.3 72.0272C104.493 71.0854 103.416 70.6145 101.936 70.6145H99.6491Z" fill="#1D1D1A"/>
<path d="M154.881 83.8669H152.863L145.396 72.4308V83.8669H143.445V69H145.396L152.863 80.5033V69H154.814V83.8669H154.881Z" fill="#1D1D1A"/>
<path d="M193.7 76.938L191.883 78.8216V83.8669H189.933V69H191.883V76.3325L198.476 69H200.831L194.978 75.5253L201.301 83.7996H198.947L193.7 76.938Z" fill="#1D1D1A"/>
<path d="M125.92 71.4218L130.494 83.8669H132.445L126.794 69H125.113L119.395 83.8669H121.413L125.92 71.4218Z" fill="#1D1D1A"/>
<path d="M172.407 71.4218L176.914 83.8669H178.932L173.282 69H171.533L165.882 83.8669H167.9L172.407 71.4218Z" fill="#1D1D1A"/>
<path d="M98.3216 96.7115H97.1582V90.8667H98.3216V96.7115Z" fill="#231F20"/>
<path d="M108.503 96.7115H107.339L104.985 92.778V96.7115H103.821V90.8667H104.985L107.339 94.8002V90.8667H108.503V96.7115Z" fill="#231F20"/>
<path d="M118.463 91.7808H116.801V96.7392H115.637V91.7808H114.003V90.8667H118.49V91.7808H118.463Z" fill="#231F20"/>
<path d="M127.314 94.1354H125.154V95.7974H127.73V96.6838H123.99V90.8667H127.73V91.7808H125.154V93.2489H127.314V94.1631V94.1354Z" fill="#231F20"/>
<path d="M134.393 95.8251H137.024V96.7115H133.229V90.8667H134.393V95.8251Z" fill="#231F20"/>
<path d="M143.688 95.8251H146.319V96.7115H142.524V90.8667H143.688V95.8251Z" fill="#231F20"/>
<path d="M152.983 96.7115H151.819V90.8667H152.983V96.7115Z" fill="#231F20"/>
<path d="M163.081 95.9913C162.887 96.2406 162.638 96.4345 162.277 96.6007C161.917 96.7669 161.446 96.8777 160.892 96.8777C160.338 96.8777 159.618 96.6561 159.147 96.2129C158.704 95.7697 158.482 95.188 158.482 94.4678V93.2489C158.482 92.5287 158.704 91.9747 159.147 91.5315C159.59 91.0883 160.144 90.8667 160.837 90.8667C161.529 90.8667 162.111 91.0329 162.471 91.3653C162.859 91.6977 163.025 92.1686 163.025 92.7226H161.917C161.917 92.4179 161.834 92.1686 161.64 92.0024C161.474 91.8362 161.197 91.7531 160.865 91.7531C160.532 91.7531 160.228 91.8916 160.006 92.1686C159.784 92.4456 159.674 92.8057 159.674 93.2489V94.4678C159.674 94.911 159.784 95.2711 160.006 95.5481C160.228 95.8251 160.532 95.9636 160.92 95.9636C161.308 95.9636 161.419 95.9359 161.585 95.8805C161.751 95.8251 161.862 95.742 161.973 95.6589V94.5232H160.837V93.7199H163.136V95.9913H163.081Z" fill="#231F20"/>
<path d="M171.96 94.1354H169.799V95.7974H172.375V96.6838H168.636V90.8667H172.375V91.7808H169.799V93.2489H171.96V94.1631V94.1354Z" fill="#231F20"/>
<path d="M182.556 96.7115H181.393L179.038 92.778V96.7115H177.875V90.8667H179.038L181.393 94.8002V90.8667H182.556V96.7115Z" fill="#231F20"/>
<path d="M192.572 94.8556C192.572 95.4927 192.406 95.9913 191.99 96.3514C191.602 96.7115 191.048 96.8777 190.383 96.8777C189.719 96.8777 189.109 96.6561 188.694 96.2129C188.251 95.7697 188.057 95.188 188.057 94.4678V93.3043C188.057 92.5841 188.278 92.0024 188.694 91.5592C189.109 91.116 189.663 90.8667 190.356 90.8667C191.048 90.8667 191.63 91.0329 192.018 91.393C192.433 91.7531 192.627 92.224 192.6 92.8611H191.464C191.464 92.4733 191.381 92.1963 191.187 92.0301C190.993 91.8362 190.716 91.7531 190.356 91.7531C189.996 91.7531 189.719 91.8916 189.525 92.1686C189.331 92.4456 189.22 92.8057 189.22 93.2766V94.4401C189.22 94.8833 189.331 95.2434 189.525 95.5481C189.719 95.8251 190.023 95.9636 190.383 95.9636C190.744 95.9636 190.993 95.8805 191.159 95.6866C191.325 95.4927 191.408 95.2157 191.408 94.8279H192.544L192.572 94.8556Z" fill="#231F20"/>
<path d="M201.427 94.1354H199.266V95.7974H201.842V96.6838H198.103V90.8667H201.842V91.7808H199.266V93.2489H201.427V94.1631V94.1354Z" fill="#231F20"/>
<defs>
<linearGradient id="paint0_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint1_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint2_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint3_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint4_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint5_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint6_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint7_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint8_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint9_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint10_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint11_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint12_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint13_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint14_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint15_linear_13943_20144" x1="134.924" y1="6.14538" x2="176.565" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

+84
View File
@@ -0,0 +1,84 @@
<svg width="62" height="62" viewBox="0 0 62 62" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M29.837 7.64759C29.837 11.8811 29.9738 16.1145 29.837 20.348C29.7001 23.489 27.6471 25.8106 25.1835 27.7225C24.7729 27.9956 24.3623 28.2687 23.678 28.815C24.9098 29.6344 25.8678 30.3172 26.9628 31.1366H0C0.4106 31 0.684334 30.8634 0.821201 30.8634C4.79034 30.0441 9.03321 29.7709 12.8655 28.1322C21.8987 24.3084 27.5102 17.6168 29.7001 8.05728L29.837 7.64759Z" fill="url(#paint0_linear_13943_20143)"/>
<path d="M33.6692 23.3524C33.2586 23.8987 32.7112 24.4449 32.4374 24.8546C32.0268 25.4009 31.89 25.9471 31.4794 26.63V0C31.4794 0 31.4794 0 31.6162 0C32.0268 2.04846 32.3006 3.96035 32.7112 6.00881C34.4904 16.3877 40.2388 23.7621 49.9564 27.859C51.5988 28.5419 53.5149 28.9515 55.2942 29.6344C55.1573 29.6344 55.0204 29.6344 54.8836 29.6344C51.4619 29.6344 48.1771 29.6344 44.7554 29.6344C40.2388 29.7709 36.954 27.7225 34.3536 24.0352C34.3536 23.8987 34.0798 23.6256 33.943 23.489C33.943 23.489 33.943 23.489 33.6692 23.3524Z" fill="url(#paint1_linear_13943_20143)"/>
<path d="M8.48601 32.3656C12.1814 32.3656 16.0137 32.3656 19.7091 32.3656C23.2676 32.3656 25.8681 34.4141 27.9211 37.1454C28.1948 37.5551 28.4686 37.9648 29.016 38.6476C29.8372 37.5551 30.5216 36.4626 31.3428 35.5066V62C30.9322 59.815 30.6584 57.7665 30.2478 55.5815C28.058 43.5639 21.0778 35.9163 9.17034 32.6388C9.03347 32.6388 8.75974 32.6388 8.62288 32.5022C8.62288 32.5022 8.62288 32.5022 8.62288 32.3656H8.48601Z" fill="url(#paint2_linear_13943_20143)"/>
<path d="M32.8474 53.8062C32.8474 49.8458 32.7105 45.8855 32.8474 41.9251C32.9842 38.6476 34.9004 36.1894 37.5008 34.2775C37.9114 34.0044 38.322 33.7313 39.0064 33.185C37.9114 32.3656 36.8165 31.6828 35.7216 31H62C60.2207 31.2731 58.5783 31.6828 56.7991 31.9559C50.3663 32.9119 44.7548 35.5066 40.2382 40.1498C36.6796 43.7004 34.3529 47.9339 33.1211 52.8502C33.1211 53.1234 32.9842 53.3965 32.8474 53.8062Z" fill="url(#paint3_linear_13943_20143)"/>
<path d="M29.837 7.64759C29.837 11.8811 29.9738 16.1145 29.837 20.348C29.7001 23.489 27.6471 25.8106 25.1835 27.7225C24.7729 27.9956 24.3623 28.2687 23.678 28.815C24.9098 29.6344 25.8678 30.3172 26.9628 31.1366H0C0.4106 31 0.684334 30.8634 0.821201 30.8634C4.79034 30.0441 9.03321 29.7709 12.8655 28.1322C21.8987 24.3084 27.5102 17.6168 29.7001 8.05728L29.837 7.64759Z" fill="url(#paint4_linear_13943_20143)"/>
<path d="M33.6692 23.3524C33.2586 23.8987 32.7112 24.4449 32.4374 24.8546C32.0268 25.4009 31.89 25.9471 31.4794 26.63V0C31.4794 0 31.4794 0 31.6162 0C32.0268 2.04846 32.3006 3.96035 32.7112 6.00881C34.4904 16.3877 40.2388 23.7621 49.9564 27.859C51.5988 28.5419 53.5149 28.9515 55.2942 29.6344C55.1573 29.6344 55.0204 29.6344 54.8836 29.6344C51.4619 29.6344 48.1771 29.6344 44.7554 29.6344C40.2388 29.7709 36.954 27.7225 34.3536 24.0352C34.3536 23.8987 34.0798 23.6256 33.943 23.489C33.943 23.489 33.943 23.489 33.6692 23.3524Z" fill="url(#paint5_linear_13943_20143)"/>
<path d="M8.48601 32.3656C12.1814 32.3656 16.0137 32.3656 19.7091 32.3656C23.2676 32.3656 25.8681 34.4141 27.9211 37.1454C28.1948 37.5551 28.4686 37.9648 29.016 38.6476C29.8372 37.5551 30.5216 36.4626 31.3428 35.5066V62C30.9322 59.815 30.6584 57.7665 30.2478 55.5815C28.058 43.5639 21.0778 35.9163 9.17034 32.6388C9.03347 32.6388 8.75974 32.6388 8.62288 32.5022C8.62288 32.5022 8.62288 32.5022 8.62288 32.3656H8.48601Z" fill="url(#paint6_linear_13943_20143)"/>
<path d="M32.8474 53.8062C32.8474 49.8458 32.7105 45.8855 32.8474 41.9251C32.9842 38.6476 34.9004 36.1894 37.5008 34.2775C37.9114 34.0044 38.322 33.7313 39.0064 33.185C37.9114 32.3656 36.8165 31.6828 35.7216 31H62C60.2207 31.2731 58.5783 31.6828 56.7991 31.9559C50.3663 32.9119 44.7548 35.5066 40.2382 40.1498C36.6796 43.7004 34.3529 47.9339 33.1211 52.8502C33.1211 53.1234 32.9842 53.3965 32.8474 53.8062Z" fill="url(#paint7_linear_13943_20143)"/>
<path d="M29.837 7.64759C29.837 11.8811 29.9738 16.1145 29.837 20.348C29.7001 23.489 27.6471 25.8106 25.1835 27.7225C24.7729 27.9956 24.3623 28.2687 23.678 28.815C24.9098 29.6344 25.8678 30.3172 26.9628 31.1366H0C0.4106 31 0.684334 30.8634 0.821201 30.8634C4.79034 30.0441 9.03321 29.7709 12.8655 28.1322C21.8987 24.3084 27.5102 17.6168 29.7001 8.05728L29.837 7.64759Z" fill="url(#paint8_linear_13943_20143)"/>
<path d="M33.6692 23.3524C33.2586 23.8987 32.7112 24.4449 32.4374 24.8546C32.0268 25.4009 31.89 25.9471 31.4794 26.63V0C31.4794 0 31.4794 0 31.6162 0C32.0268 2.04846 32.3006 3.96035 32.7112 6.00881C34.4904 16.3877 40.2388 23.7621 49.9564 27.859C51.5988 28.5419 53.5149 28.9515 55.2942 29.6344C55.1573 29.6344 55.0204 29.6344 54.8836 29.6344C51.4619 29.6344 48.1771 29.6344 44.7554 29.6344C40.2388 29.7709 36.954 27.7225 34.3536 24.0352C34.3536 23.8987 34.0798 23.6256 33.943 23.489C33.943 23.489 33.943 23.489 33.6692 23.3524Z" fill="url(#paint9_linear_13943_20143)"/>
<path d="M8.48601 32.3656C12.1814 32.3656 16.0137 32.3656 19.7091 32.3656C23.2676 32.3656 25.8681 34.4141 27.9211 37.1454C28.1948 37.5551 28.4686 37.9648 29.016 38.6476C29.8372 37.5551 30.5216 36.4626 31.3428 35.5066V62C30.9322 59.815 30.6584 57.7665 30.2478 55.5815C28.058 43.5639 21.0778 35.9163 9.17034 32.6388C9.03347 32.6388 8.75974 32.6388 8.62288 32.5022C8.62288 32.5022 8.62288 32.5022 8.62288 32.3656H8.48601Z" fill="url(#paint10_linear_13943_20143)"/>
<path d="M32.8474 53.8062C32.8474 49.8458 32.7105 45.8855 32.8474 41.9251C32.9842 38.6476 34.9004 36.1894 37.5008 34.2775C37.9114 34.0044 38.322 33.7313 39.0064 33.185C37.9114 32.3656 36.8165 31.6828 35.7216 31H62C60.2207 31.2731 58.5783 31.6828 56.7991 31.9559C50.3663 32.9119 44.7548 35.5066 40.2382 40.1498C36.6796 43.7004 34.3529 47.9339 33.1211 52.8502C33.1211 53.1234 32.9842 53.3965 32.8474 53.8062Z" fill="url(#paint11_linear_13943_20143)"/>
<path d="M29.837 7.64759C29.837 11.8811 29.9738 16.1145 29.837 20.348C29.7001 23.489 27.6471 25.8106 25.1835 27.7225C24.7729 27.9956 24.3623 28.2687 23.678 28.815C24.9098 29.6344 25.8678 30.3172 26.9628 31.1366H0C0.4106 31 0.684334 30.8634 0.821201 30.8634C4.79034 30.0441 9.03321 29.7709 12.8655 28.1322C21.8987 24.3084 27.5102 17.6168 29.7001 8.05728L29.837 7.64759Z" fill="url(#paint12_linear_13943_20143)"/>
<path d="M33.6692 23.3524C33.2586 23.8987 32.7112 24.4449 32.4374 24.8546C32.0268 25.4009 31.89 25.9471 31.4794 26.63V0C31.4794 0 31.4794 0 31.6162 0C32.0268 2.04846 32.3006 3.96035 32.7112 6.00881C34.4904 16.3877 40.2388 23.7621 49.9564 27.859C51.5988 28.5419 53.5149 28.9515 55.2942 29.6344C55.1573 29.6344 55.0204 29.6344 54.8836 29.6344C51.4619 29.6344 48.1771 29.6344 44.7554 29.6344C40.2388 29.7709 36.954 27.7225 34.3536 24.0352C34.3536 23.8987 34.0798 23.6256 33.943 23.489C33.943 23.489 33.943 23.489 33.6692 23.3524Z" fill="url(#paint13_linear_13943_20143)"/>
<path d="M8.48601 32.3656C12.1814 32.3656 16.0137 32.3656 19.7091 32.3656C23.2676 32.3656 25.8681 34.4141 27.9211 37.1454C28.1948 37.5551 28.4686 37.9648 29.016 38.6476C29.8372 37.5551 30.5216 36.4626 31.3428 35.5066V62C30.9322 59.815 30.6584 57.7665 30.2478 55.5815C28.058 43.5639 21.0778 35.9163 9.17034 32.6388C9.03347 32.6388 8.75974 32.6388 8.62288 32.5022C8.62288 32.5022 8.62288 32.5022 8.62288 32.3656H8.48601Z" fill="url(#paint14_linear_13943_20143)"/>
<path d="M32.8474 53.8062C32.8474 49.8458 32.7105 45.8855 32.8474 41.9251C32.9842 38.6476 34.9004 36.1894 37.5008 34.2775C37.9114 34.0044 38.322 33.7313 39.0064 33.185C37.9114 32.3656 36.8165 31.6828 35.7216 31H62C60.2207 31.2731 58.5783 31.6828 56.7991 31.9559C50.3663 32.9119 44.7548 35.5066 40.2382 40.1498C36.6796 43.7004 34.3529 47.9339 33.1211 52.8502C33.1211 53.1234 32.9842 53.3965 32.8474 53.8062Z" fill="url(#paint15_linear_13943_20143)"/>
<defs>
<linearGradient id="paint0_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint1_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint2_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint3_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint4_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint5_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint6_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint7_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint8_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint9_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint10_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint11_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint12_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint13_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint14_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
<linearGradient id="paint15_linear_13943_20143" x1="16.424" y1="6.14538" x2="58.0646" y2="53.3515" gradientUnits="userSpaceOnUse">
<stop stop-color="#B52B9D"/>
<stop offset="1" stop-color="#8393CF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 200 200"><rect width="200" height="200" fill="url('#gradient')"></rect><defs><linearGradient id="SvgjsLinearGradient1001" gradientTransform="rotate(101 0.5 0.5)"><stop offset="0%" stop-color="#b7b8ba"></stop><stop offset="100%" stop-color="#ebf1f1"></stop></linearGradient></defs><g><g fill="#242f35" transform="matrix(7.071823204419889,0,0,7.071823204419889,-7.597636903188501,150.28003879410127)" stroke="#323847" stroke-width="0.2"><path d="M5.75 0L1.07 0L1.07-14.22L5.65-14.22Q7.54-14.22 9.04-13.36Q10.55-12.51 11.39-10.95Q12.24-9.39 12.25-7.46L12.25-7.46L12.25-6.81Q12.25-4.85 11.42-3.31Q10.60-1.76 9.10-0.88Q7.60-0.01 5.75 0L5.75 0ZM5.65-11.57L4.50-11.57L4.50-2.64L5.69-2.64Q7.17-2.64 7.96-3.69Q8.75-4.74 8.75-6.81L8.75-6.81L8.75-7.42Q8.75-9.48 7.96-10.53Q7.17-11.57 5.65-11.57L5.65-11.57ZM14.00-14.22L18.51-14.22L21.68-4.33L24.84-14.22L29.36-14.22L29.36 0L25.92 0L25.92-3.32L26.25-10.12L22.81 0L20.55 0L17.10-10.13L17.43-3.32L17.43 0L14.00 0L14.00-14.22Z"></path></g></g></svg><style>@media (prefers-color-scheme: light) { :root { filter: none; } }
@media (prefers-color-scheme: dark) { :root { filter: none; } }
</style></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

+5 -4
View File
@@ -4,6 +4,7 @@ import { type FC } from 'react'
import { useRouter } from 'next/navigation'
import Seperator from '@/components/utils/Seperator'
import Image from 'next/image'
import { glassSurfaceCard } from '@/lib/styles/glassSurface'
const GamePage: FC = () => {
@@ -22,7 +23,7 @@ const GamePage: FC = () => {
/>
</div>
<div className='mt-11 relative bg-primary text-white dark:bg-primary dark:text-primary-foreground h-[120px] rounded-[10px] p-4'>
<div className='mt-11 relative overflow-hidden bg-primary text-white dark:bg-primary dark:text-primary-foreground h-[120px] rounded-[10px] p-4'>
<div className='max-w-[99px]'>
<div className='font-bold'>My Games</div>
<div className='mt-1'>
@@ -46,7 +47,7 @@ const GamePage: FC = () => {
<div className='mt-8 flex flex-col gap-4'>
<div
style={{ direction: 'ltr' }}
className='bg-white dark:bg-container shadow-sm rounded-[10px] p-2 flex justify-between items-center cursor-pointer border border-transparent dark:border-border/50'
className={glassSurfaceCard('rounded-[10px] p-2 flex justify-between items-center cursor-pointer')}
onClick={() => {
const currentPath = window.location.pathname
const basePath = currentPath.split('/game')[0]
@@ -76,7 +77,7 @@ const GamePage: FC = () => {
<div
style={{ direction: 'ltr' }}
className='bg-white dark:bg-container shadow-sm rounded-[10px] p-2 flex justify-between items-center cursor-pointer border border-transparent dark:border-border/50'
className={glassSurfaceCard('rounded-[10px] p-2 flex justify-between items-center cursor-pointer')}
onClick={() => {
const currentPath = window.location.pathname
const basePath = currentPath.split('/game')[0]
@@ -106,7 +107,7 @@ const GamePage: FC = () => {
<div
style={{ direction: 'ltr' }}
className='bg-white dark:bg-container shadow-sm rounded-[10px] p-2 flex justify-between items-center cursor-pointer border border-transparent dark:border-border/50'
className={glassSurfaceCard('rounded-[10px] p-2 flex justify-between items-center cursor-pointer')}
onClick={() => {
const currentPath = window.location.pathname
const basePath = currentPath.split('/game')[0]
+2 -12
View File
@@ -2,25 +2,15 @@
import ClientSideWrapper from '@/components/wrapper/ClientSideWrapper';
import React from 'react';
import { usePathname } from 'next/navigation';
import clsx from 'clsx';
function DialogsLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const isCheckoutPage = pathname?.includes('/checkout/');
return (
<ClientSideWrapper>
<main className={clsx(
'h-full dark:bg-background w-full overflow-y-auto noscrollbar pt-4 pb-6 px-6',
isCheckoutPage
? 'bg-background dark:bg-background'
: 'bg-container lg:bg-background'
)}>
<main className='h-full bg-background dark:bg-background w-full overflow-y-auto noscrollbar pt-4 pb-6 px-6'>
{children}
</main>
</ClientSideWrapper>
)
}
export default DialogsLayout
export default DialogsLayout
@@ -10,6 +10,7 @@ import { useGetNotifications, useReadAllNotifications } from './hooks/useNotific
import type { Notification } from './types/Types';
import { useGetNotificationsCount } from '../../(Main)/hooks/useMenuData';
import Prompt from '@/components/utils/Prompt';
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
type Props = object
@@ -110,7 +111,7 @@ export default function NotificationsIndex({ }: Props) {
const formattedTime = formatTime(notification.createdAt);
return (
<li key={notification.id} className='bg-container py-5 px-4 rounded-container shadow-lg'>
<li key={notification.id} className={glassSurfaceCard('py-5 px-4')}>
{/* <h5 className='font-medium text-sm'>{notification.title}</h5> */}
<p className='text-xs mt-2 leading-5'>{notification.content}</p>
@@ -8,12 +8,13 @@ import Link from 'next/link';
import { useParams } from 'next/navigation';
import { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
import { useCheckoutStore } from '../../store/Store';
import useToggle from '@/hooks/helpers/useToggle';
import { AddressSelectionModal } from './AddressSelectionModal';
const formatAddress = (address: Address) => {
return `${address.address}، ${address.city}، ${address.province}`;
return address.address;
};
export const AddressSection = () => {
@@ -66,7 +67,7 @@ export const AddressSection = () => {
// اگر آدرس ندارد
if (addresses.length === 0) {
return (
<section className="bg-container rounded-container box-shadow-normal p-4">
<section className={glassSurfaceCard('p-4')}>
<div className="py-3">
<h3 className="text-sm2 font-medium leading-5 mb-4">{t("SectionAddress.Title")}</h3>
<p className='text-sm2 text-disabled-text mb-4'>
@@ -89,7 +90,7 @@ export const AddressSection = () => {
return (
<>
<section className="bg-container rounded-container box-shadow-normal p-4">
<section className={glassSurfaceCard('p-4')}>
<div className="py-3">
<div className='w-full flex justify-between items-center gap-2'>
<h3 className="text-sm2 font-medium leading-5 inline-block">{t("SectionAddress.Title")}</h3>
@@ -9,7 +9,7 @@ import { ef } from '@/lib/helpers/utfNumbers';
import { Edit2 } from 'iconsax-react';
const formatAddress = (address: Address) => {
return `${address.address}، ${address.city}، ${address.province}`;
return address.address;
};
interface AddressSelectionModalProps {
@@ -1,6 +1,7 @@
'use client';
import InputField from '@/components/input/InputField';
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
import { useCheckoutStore } from '../../store/Store';
import { useCallback, useEffect } from 'react';
@@ -42,7 +43,7 @@ export const CarAddressSection = () => {
}, [carAddress, setCarAddress]);
return (
<section className="bg-container rounded-container box-shadow-normal p-4">
<section className={glassSurfaceCard('p-4')}>
<div className="py-3">
<h3 className="text-sm2 font-medium leading-5 mb-4">
اطلاعات خودرو
@@ -8,6 +8,7 @@ import { useApplyCoupon, useRemoveCoupon } from '../../hooks/useOrderData';
import Button from '@/components/button/PrimaryButton';
import { extractErrorMessage } from '@/lib/func';
import { toast } from '@/components/Toast';
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
import { useEffect, useState } from 'react';
import { useGetCartItems } from '@/app/[name]/(Main)/cart/hooks/useCartData';
@@ -73,7 +74,7 @@ export const CouponSection = ({
}, [cartItems, onCouponCodeChange]);
return (
<section className="bg-container rounded-container box-shadow-normal p-4">
<section className={glassSurfaceCard('p-4')}>
<div className="py-3">
<h3 className="text-sm2 font-medium leading-5">{t("SectionCoupon.Title")}</h3>
@@ -1,249 +1,292 @@
'use client';
"use client";
import { Card, Icon, Wallet2 } from 'iconsax-react';
import { useCallback, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { useGetPaymentMethod } from '../../hooks/useOrderData';
import { useCheckoutStore } from '../../store/Store';
import { useGetUserWallet } from '@/app/[name]/(Main)/transactions/hooks/useTransactionData';
import { useGetCartItems } from '@/app/[name]/(Main)/cart/hooks/useCartData';
import { useGetProfile, useSingleUpload } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
import { PaymentMethodEnum } from '../../enum/Enum';
import { toast } from '@/components/Toast';
import { extractErrorMessage } from '@/lib/func';
import Image from 'next/image';
import { useGetCartItems } from "@/app/[name]/(Main)/cart/hooks/useCartData";
import { useGetUserWallet } from "@/app/[name]/(Main)/transactions/hooks/useTransactionData";
import { useGetProfile, useSingleUpload } from "@/app/[name]/(Profile)/profile/hooks/userProfileData";
import { toast } from "@/components/Toast";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { extractErrorMessage } from "@/lib/func";
import { Card, Icon, Wallet2 } from "iconsax-react";
import Image from "next/image";
import { glassSurfaceCard } from "@/lib/styles/glassSurface";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { PaymentMethodEnum } from "../../enum/Enum";
import { useGetPaymentMethod } from "../../hooks/useOrderData";
import { useCheckoutStore } from "../../store/Store";
type PaymentSectionProps = {
paymentType: string;
onPaymentTypeChange: (id: string) => void;
paymentType: string;
onPaymentTypeChange: (id: string) => void;
};
type CreditCardOption = {
cardNumber: string;
cardOwner: string;
};
const getIconByMethod = (method: PaymentMethodEnum): Icon => {
if (method === PaymentMethodEnum.Online || method === PaymentMethodEnum.CreditCard) {
return Card;
}
if (method === PaymentMethodEnum.Cash || method === PaymentMethodEnum.Wallet) {
return Wallet2;
}
if (method === PaymentMethodEnum.Online || method === PaymentMethodEnum.CreditCard) {
return Card;
}
if (method === PaymentMethodEnum.Cash || method === PaymentMethodEnum.Wallet) {
return Wallet2;
}
return Card;
};
export const PaymentSection = ({ paymentType, onPaymentTypeChange }: PaymentSectionProps) => {
const { t: tOrderDetail } = useTranslation("parallels", { keyPrefix: "OrderDetail" });
const { t: tOrders } = useTranslation("orders");
const { data: paymentMethod } = useGetPaymentMethod();
const { setIsSelectedPayment, paymentDesc, setPaymentDesc, paymentAttachments, setPaymentAttachments } = useCheckoutStore();
const { data: userWallet } = useGetUserWallet();
const { isSuccess } = useGetProfile();
const { data: cartData } = useGetCartItems(isSuccess);
const { mutateAsync: singleUpload, isPending: isUploading } = useSingleUpload();
const fileInputRef = useRef<HTMLInputElement>(null);
const [selectedCardNumber, setSelectedCardNumber] = useState<string>("");
const { t: tOrderDetail } = useTranslation('parallels', { keyPrefix: 'OrderDetail' });
const { t: tOrders } = useTranslation('orders');
const { data: paymentMethod } = useGetPaymentMethod();
const {
setIsSelectedPayment,
paymentDesc,
setPaymentDesc,
paymentAttachments,
setPaymentAttachments,
} = useCheckoutStore();
const { data: userWallet } = useGetUserWallet();
const { isSuccess } = useGetProfile();
const { data: cartData } = useGetCartItems(isSuccess);
const { mutateAsync: singleUpload, isPending: isUploading } = useSingleUpload();
const fileInputRef = useRef<HTMLInputElement>(null);
const walletAmount = userWallet?.data?.balance ?? 0;
const totalAmount = cartData?.data?.total ?? 0;
const isWalletInsufficient = walletAmount < totalAmount;
const walletAmount = userWallet?.data?.balance ?? 0;
const totalAmount = cartData?.data?.total ?? 0;
const isWalletInsufficient = walletAmount < totalAmount;
const formatPrice = useCallback((value: number) => value.toLocaleString("fa-IR"), []);
const formatPrice = useCallback(
(value: number) => value.toLocaleString('fa-IR'),
[]
const getPaymentMethodTranslation = useCallback(
(method: string, fallbackTitle: string) => {
const translationKey = `paymentMethod.${method}`;
const translation = tOrders(translationKey);
return translation !== translationKey ? translation : fallbackTitle;
},
[tOrders],
);
const paymentOptions = useMemo(() => {
if (!paymentMethod?.data) return [];
return paymentMethod.data
.filter((item) => item.enabled)
.sort((a, b) => a.order - b.order)
.map((item) => ({
id: item.id,
label: getPaymentMethodTranslation(item.method, item.description),
icon: getIconByMethod(item.method),
method: item.method,
cardNumber: item.cardNumber,
cardOwner: item.cardOwner,
cards:
item.cards?.map((card) => ({
cardNumber: card.cardNumber,
cardOwner: card.owner,
})) ?? [],
description: item.description,
isWallet: item.method === PaymentMethodEnum.Wallet,
}));
}, [paymentMethod, getPaymentMethodTranslation]);
const selectedCreditCard = useMemo(() => {
const selected = paymentOptions.find((opt) => opt.id === paymentType);
if (selected?.method === PaymentMethodEnum.CreditCard) return selected;
return null;
}, [paymentOptions, paymentType]);
const handlePaymentTypeChange = (value: string) => {
const selected = paymentOptions.find((opt) => opt.id === value);
if (selected?.method !== PaymentMethodEnum.CreditCard) {
setPaymentDesc("");
setPaymentAttachments([]);
setSelectedCardNumber("");
}
onPaymentTypeChange(value);
setIsSelectedPayment(true);
};
const creditCardOptions = useMemo<CreditCardOption[]>(() => {
if (!selectedCreditCard) return [];
const normalizedCards = selectedCreditCard.cards.filter(
(card): card is CreditCardOption => !!card.cardNumber && !!card.cardOwner,
);
const getPaymentMethodTranslation = useCallback((method: string, fallbackTitle: string) => {
const translationKey = `paymentMethod.${method}`;
const translation = tOrders(translationKey);
return translation !== translationKey ? translation : fallbackTitle;
}, [tOrders]);
if (normalizedCards.length > 0) return normalizedCards;
const paymentOptions = useMemo(() => {
if (!paymentMethod?.data) return [];
if (selectedCreditCard.cardNumber && selectedCreditCard.cardOwner) {
return [{ cardNumber: selectedCreditCard.cardNumber, cardOwner: selectedCreditCard.cardOwner }];
}
return paymentMethod.data
.filter(item => item.enabled)
.sort((a, b) => a.order - b.order)
.map(item => ({
id: item.id,
label: getPaymentMethodTranslation(item.method, item.description),
icon: getIconByMethod(item.method),
method: item.method,
cardNumber: item.cardNumber,
cardOwner: item.cardOwner,
description: item.description,
isWallet: item.method === PaymentMethodEnum.Wallet,
}));
}, [paymentMethod, getPaymentMethodTranslation]);
return [];
}, [selectedCreditCard]);
const selectedCreditCard = useMemo(() => {
const selected = paymentOptions.find(opt => opt.id === paymentType);
if (selected?.method === PaymentMethodEnum.CreditCard) return selected;
return null;
}, [paymentOptions, paymentType]);
useEffect(() => {
if (creditCardOptions.length === 0) {
setSelectedCardNumber("");
return;
}
const handlePaymentTypeChange = (value: string) => {
const selected = paymentOptions.find(opt => opt.id === value);
if (selected?.method !== PaymentMethodEnum.CreditCard) {
setPaymentDesc('');
setPaymentAttachments([]);
}
onPaymentTypeChange(value);
setIsSelectedPayment(true);
};
const hasSelectedCard = creditCardOptions.some((item) => item.cardNumber === selectedCardNumber);
if (!hasSelectedCard) {
setSelectedCardNumber(creditCardOptions[0].cardNumber);
}
}, [creditCardOptions, selectedCardNumber]);
const handleReceiptUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const selectedCard = useMemo(
() => creditCardOptions.find((item) => item.cardNumber === selectedCardNumber) ?? null,
[creditCardOptions, selectedCardNumber],
);
try {
const response = await singleUpload(file);
if (response?.data?.url) {
setPaymentAttachments([response.data.url]);
toast(tOrderDetail('SectionPayment.CreditCard.UploadSuccess'), 'success');
}
} catch (error) {
toast(extractErrorMessage(error), 'error');
} finally {
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
};
const handleCopyCardNumber = useCallback(
async (cardNumber: string) => {
try {
await navigator.clipboard.writeText(cardNumber);
toast(tOrderDetail("SectionPayment.CreditCard.CopySuccess"), "success");
} catch {
toast(tOrderDetail("SectionPayment.CreditCard.CopyError"), "error");
}
},
[tOrderDetail],
);
return (
<section className="bg-container rounded-container box-shadow-normal p-4">
<div className="py-3">
<h3 className="text-sm2 font-medium leading-5">{tOrderDetail("SectionPayment.Title")}</h3>
const handleReceiptUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
<RadioGroup
value={paymentType}
onValueChange={handlePaymentTypeChange}
className='flex flex-col gap-6 mt-6'
>
{paymentOptions.map(({ id, label, icon: Icon, isWallet }) => {
const isDisabled = isWallet && isWalletInsufficient;
try {
const response = await singleUpload(file);
if (response?.data?.url) {
setPaymentAttachments([response.data.url]);
toast(tOrderDetail("SectionPayment.CreditCard.UploadSuccess"), "success");
}
} catch (error) {
toast(extractErrorMessage(error), "error");
} finally {
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
}
};
return (
<div key={id} className='flex items-center justify-between'>
<RadioGroupItem
value={id}
id={`paymentMethod${id}`}
disabled={isDisabled}
/>
<label
className={`flex items-center gap-2 ${isDisabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}`}
htmlFor={`paymentMethod${id}`}
>
<div className='flex gap-1'>
{isWallet && (
<span className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
({formatPrice(walletAmount)} T)
</span>
)}
<span className='text-xs mt-0.5'>{label}</span>
</div>
<Icon
size={16}
color='currentColor'
className='mr-2'
/>
</label>
</div>
);
})}
</RadioGroup>
return (
<section className={glassSurfaceCard('p-4')}>
<div className="py-3">
<h3 className="text-sm2 font-medium leading-5">{tOrderDetail("SectionPayment.Title")}</h3>
{selectedCreditCard && (
<div className='mt-6 pt-4 border-t border-border flex flex-col gap-4'>
{selectedCreditCard.cardOwner && (
<div>
<span className='text-xs text-gray-500 dark:text-gray-400'>
{tOrderDetail('SectionPayment.CreditCard.CardOwnerLabel')}
</span>
<p className='text-sm font-medium mt-1'>
{selectedCreditCard.cardOwner}
</p>
</div>
)}
{selectedCreditCard.cardNumber && (
<div>
<span className='text-xs text-gray-500 dark:text-gray-400'>
{tOrderDetail('SectionPayment.CreditCard.CardNumberLabel')}
</span>
<p className='text-sm font-medium mt-1' dir='ltr'>
{selectedCreditCard.cardNumber}
</p>
</div>
)}
{selectedCreditCard.description && (
<RadioGroup value={paymentType} onValueChange={handlePaymentTypeChange} className="flex flex-col gap-6 mt-6">
{paymentOptions.map(({ id, label, icon: Icon, isWallet }) => {
const isDisabled = isWallet && isWalletInsufficient;
return (
<div key={id} className="flex items-center justify-between">
<RadioGroupItem value={id} id={`paymentMethod${id}`} disabled={isDisabled} />
<label className={`flex items-center gap-2 ${isDisabled ? "cursor-not-allowed opacity-50" : "cursor-pointer"}`} htmlFor={`paymentMethod${id}`}>
<div className="flex gap-1">
{isWallet && <span className="text-xs text-gray-500 dark:text-gray-400 mt-1">({formatPrice(walletAmount)} T)</span>}
<span className="text-xs mt-0.5">{label}</span>
</div>
<Icon size={16} color="currentColor" className="mr-2" />
</label>
</div>
);
})}
</RadioGroup>
{selectedCreditCard && (
<div className="mt-6 pt-4 border-t border-border flex flex-col gap-4">
{creditCardOptions.length > 0 && (
<div className="space-y-3">
<p className="text-xs text-gray-500 dark:text-gray-400">
{`تعداد کارت های مقصد: ${creditCardOptions.length}`}
</p>
{creditCardOptions.map((card) => {
const isActiveCard = selectedCardNumber === card.cardNumber;
return (
<button
key={card.cardNumber}
type="button"
onClick={() => setSelectedCardNumber(card.cardNumber)}
className={`w-full rounded-normal border px-3 py-2 text-right transition-colors ${
isActiveCard ? "border-primary bg-primary/5" : "border-border hover:border-primary/50"
}`}
>
<span className="block text-xs text-gray-500 dark:text-gray-400">{card.cardOwner}</span>
<span className="block text-sm font-medium mt-1" dir="ltr">
{card.cardNumber}
</span>
</button>
);
})}
</div>
)}
{selectedCard && (
<div className="flex items-start justify-between gap-4">
{selectedCard.cardOwner && (
<div>
<span className="text-xs text-gray-500 dark:text-gray-400">{tOrderDetail("SectionPayment.CreditCard.CardOwnerLabel")}</span>
<p className="text-sm font-medium mt-1">{selectedCard.cardOwner}</p>
</div>
)}
{selectedCard.cardNumber && (
<div className="text-left">
<span className="text-xs text-gray-500 dark:text-gray-400">{tOrderDetail("SectionPayment.CreditCard.CardNumberLabel")}</span>
<button
type="button"
className="block text-sm font-medium mt-1 cursor-pointer hover:text-primary transition-colors"
dir="ltr"
onClick={() => handleCopyCardNumber(selectedCard.cardNumber)}
>
{selectedCard.cardNumber}
</button>
</div>
)}
</div>
)}
{/* {selectedCreditCard.description && (
<p className='text-xs text-gray-500 dark:text-gray-400 leading-5'>
{selectedCreditCard.description}
</p>
)}
<div>
<label className='text-xs text-gray-500 dark:text-gray-400' htmlFor='paymentDesc'>
{tOrderDetail('SectionPayment.CreditCard.PaymentDescLabel')}
</label>
<textarea
id='paymentDesc'
className='w-full px-4 py-2.5 mt-2 text-sm2 leading-6 outline-0 border border-border rounded-normal resize-none focus:ring-0'
placeholder={tOrderDetail('SectionPayment.CreditCard.PaymentDescPlaceholder')}
value={paymentDesc}
onChange={(e) => setPaymentDesc(e.target.value)}
rows={2}
/>
</div>
<div>
<span className='text-xs text-gray-500 dark:text-gray-400'>
{tOrderDetail('SectionPayment.CreditCard.ReceiptLabel')}
</span>
<input
ref={fileInputRef}
type='file'
accept='image/*'
className='hidden'
onChange={handleReceiptUpload}
/>
{paymentAttachments[0] ? (
<div className='mt-2 relative w-full h-32 rounded-normal overflow-hidden border border-border'>
<Image
src={paymentAttachments[0]}
alt='receipt'
fill
className='object-contain'
/>
<button
type='button'
className='absolute top-2 left-2 text-xs bg-white/90 dark:bg-background/90 px-2 py-1 rounded-normal'
onClick={() => fileInputRef.current?.click()}
disabled={isUploading}
>
{isUploading
? tOrderDetail('SectionPayment.CreditCard.Uploading')
: tOrderDetail('SectionPayment.CreditCard.UploadReceipt')}
</button>
</div>
) : (
<button
type='button'
className='w-full mt-2 py-3 text-sm2 border border-dashed border-border rounded-normal text-gray-500 dark:text-gray-400 hover:border-primary transition-colors'
onClick={() => fileInputRef.current?.click()}
disabled={isUploading}
>
{isUploading
? tOrderDetail('SectionPayment.CreditCard.Uploading')
: tOrderDetail('SectionPayment.CreditCard.UploadReceipt')}
</button>
)}
</div>
</div>
)}
)} */}
<div>
<label className="text-xs text-gray-500 dark:text-gray-400" htmlFor="paymentDesc">
{tOrderDetail("SectionPayment.CreditCard.PaymentDescLabel")}
</label>
<textarea
id="paymentDesc"
className="w-full px-2 py-2.5 mt-2 text-xs leading-6 outline-0 border border-border rounded-normal resize-none focus:ring-0"
placeholder={tOrderDetail("SectionPayment.CreditCard.PaymentDescPlaceholder")}
value={paymentDesc}
onChange={(e) => setPaymentDesc(e.target.value)}
rows={2}
/>
</div>
</section>
);
<div>
<span className="text-xs text-gray-500 dark:text-gray-400">{tOrderDetail("SectionPayment.CreditCard.ReceiptLabel")}</span>
<input ref={fileInputRef} type="file" accept="image/*" className="hidden" onChange={handleReceiptUpload} />
{paymentAttachments[0] ? (
<div className="mt-2 relative w-full h-32 rounded-normal overflow-hidden border border-border">
<Image src={paymentAttachments[0]} alt="receipt" fill className="object-contain" />
<button
type="button"
className="absolute top-2 left-2 text-xs bg-white/90 dark:bg-background/90 px-2 py-1 rounded-normal"
onClick={() => fileInputRef.current?.click()}
disabled={isUploading}
>
{isUploading ? tOrderDetail("SectionPayment.CreditCard.Uploading") : tOrderDetail("SectionPayment.CreditCard.UploadReceipt")}
</button>
</div>
) : (
<button
type="button"
className="w-full mt-2 py-3 text-sm2 border border-dashed border-border rounded-normal text-gray-500 dark:text-gray-400 hover:border-primary transition-colors"
onClick={() => fileInputRef.current?.click()}
disabled={isUploading}
>
{isUploading ? tOrderDetail("SectionPayment.CreditCard.Uploading") : tOrderDetail("SectionPayment.CreditCard.UploadReceipt")}
</button>
)}
</div>
</div>
)}
</div>
</section>
);
};
@@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
import { useGetShipmentMethod } from '../../hooks/useOrderData';
import { DeliveryMethod } from '../../types/Types';
import { useCheckoutStore } from '../../store/Store';
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
type ShippingSectionProps = {
deliveryType: string;
@@ -57,7 +58,7 @@ export const ShippingSection = ({ deliveryType, onDeliveryTypeChange }: Shipping
})) || [];
return (
<section className="bg-container rounded-container box-shadow-normal p-4">
<section className={glassSurfaceCard('p-4')}>
<div className="py-3">
<h3 className="text-sm2 font-medium leading-5">{tOrderDetail("SectionShipping.Title")}</h3>
<Combobox
@@ -80,4 +81,3 @@ export const ShippingSection = ({ deliveryType, onDeliveryTypeChange }: Shipping
</section>
);
};
@@ -3,6 +3,7 @@
import { MinusIcon, PlusIcon } from 'lucide-react';
import { motion } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
type TableSectionProps = {
tableNumber: number;
@@ -14,7 +15,7 @@ export const TableSection = ({ tableNumber, onIncrement, onDecrement }: TableSec
const { t } = useTranslation('parallels', { keyPrefix: 'OrderDetail' });
return (
<section className="bg-container rounded-container box-shadow-normal p-4 py-2">
<section className={glassSurfaceCard('p-4 py-2')}>
<div className="py-3">
<div className='w-full grid grid-cols-2 justify-between items-center gap-2'>
<h3 className="text-sm2 font-medium leading-5 inline-block">{t("SectionTable.Title")}</h3>
@@ -22,7 +23,7 @@ export const TableSection = ({ tableNumber, onIncrement, onDecrement }: TableSec
<div className='w-full flex justify-end'>
<motion.div
whileTap={{ scale: 1.05 }}
className="bg-background active:drop-shadow-xs max-w-[132px] rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2"
className="bg-background pattern-secondary-bg active:drop-shadow-xs max-w-[132px] rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2"
>
<button
onClick={onIncrement}
@@ -116,10 +116,21 @@ export interface PaymentMethodResponse {
enabled: boolean;
order: number;
merchantId: string | null;
cards?: PaymentMethodCard[];
}
export type PaymentMethodsResponse = BaseResponse<PaymentMethodResponse[]>;
export interface PaymentMethodCard {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
paymentMethod: string;
cardNumber: string;
owner: string;
}
export type CheckoutStoreType = {
isSelectedAddress: boolean;
setIsSelectedAddress: (value: boolean) => void;
@@ -196,8 +207,6 @@ export interface OrderAddress {
user: OrderUser;
title: string;
address: string;
city: string;
province: string;
postalCode: string;
latitude: number;
longitude: number;
@@ -439,12 +448,10 @@ export interface OrderDetailCarAddress {
}
export interface OrderDetailUserAddress {
city: string;
phone: string;
address: string;
fullName: string;
latitude: number;
province: string;
longitude: number;
postalCode: string;
}
@@ -468,6 +475,7 @@ export interface OrderDetail {
subTotal: number;
tax: number;
deliveryFee: number;
prepareTime: number | null;
total: number;
totalItems: number;
description: string;
+193 -210
View File
@@ -1,38 +1,39 @@
'use client';
"use client";
import { useParams, useRouter } from 'next/navigation';
import Image from 'next/image';
import React from 'react';
import Button from '@/components/button/PrimaryButton';
import { Skeleton } from '@/components/ui/skeleton';
import { Clock } from 'iconsax-react';
import clsx from 'clsx';
import useToggle from '@/hooks/helpers/useToggle';
import Prompt from '@/components/utils/Prompt';
import { useCancelOrder, useGetOrderDetail } from '../../checkout/hooks/useOrderData';
import ordersTranslations from '@/locales/fa/orders.json';
import { toast } from '@/components/Toast';
import { extractErrorMessage } from '@/lib/func';
import { useQueryClient } from '@tanstack/react-query';
import Button from "@/components/button/PrimaryButton";
import { toast } from "@/components/Toast";
import { Skeleton } from "@/components/ui/skeleton";
import Prompt from "@/components/utils/Prompt";
import useToggle from "@/hooks/helpers/useToggle";
import { extractErrorMessage } from "@/lib/func";
import ordersTranslations from "@/locales/fa/orders.json";
import { useQueryClient } from "@tanstack/react-query";
import clsx from "clsx";
import { Clock } from "iconsax-react";
import Image from "next/image";
import { useParams, useRouter } from "next/navigation";
import React from "react";
import { OrderStatus } from "../../checkout/enum/Enum";
import { useCancelOrder, useGetOrderDetail } from "../../checkout/hooks/useOrderData";
const getDeliveryMethodTitle = (method: string) => {
switch (method) {
case 'deliveryCourier':
return 'ارسال توسط پیک';
case 'deliveryCar':
return 'تحویل به خودرو';
case 'pickup':
return 'تحویل حضوری';
case 'dineIn':
return 'سرو در رستوران';
default:
return 'روش ارسال';
}
switch (method) {
case "deliveryCourier":
return "ارسال توسط پیک";
case "deliveryCar":
return "تحویل به خودرو";
case "pickup":
return "تحویل حضوری";
case "dineIn":
return "سرو در رستوران";
default:
return "روش ارسال";
}
};
const getPaymentMethodTitle = (method: string, fallback?: string) => {
const methodKey = method as keyof typeof ordersTranslations.paymentMethod;
return ordersTranslations.paymentMethod[methodKey] || fallback || 'نامشخص';
const methodKey = method as keyof typeof ordersTranslations.paymentMethod;
return ordersTranslations.paymentMethod[methodKey] || fallback || "نامشخص";
};
// const getStatusPercentage = (status: string) => {
@@ -59,17 +60,17 @@ const getPaymentMethodTitle = (method: string, fallback?: string) => {
// };
const getStatusText = (status: string): string => {
const statusKey = status as keyof typeof ordersTranslations.status;
return ordersTranslations.status[statusKey] || 'نامشخص';
const statusKey = status as keyof typeof ordersTranslations.status;
return ordersTranslations.status[statusKey] || "نامشخص";
};
const formatDate = (dateString: string): string => {
const date = new Date(dateString);
return date.toLocaleDateString('fa-IR', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
const date = new Date(dateString);
return date.toLocaleDateString("fa-IR", {
year: "numeric",
month: "long",
day: "numeric",
});
};
// const formatTime = (dateString: string): string => {
@@ -81,191 +82,173 @@ const formatDate = (dateString: string): string => {
// };
function OrderTrackingPage() {
const { id, name } = useParams();
const router = useRouter();
const queryClient = useQueryClient();
const { state: cancelModal, toggle: toggleCancelModal } = useToggle();
const { data: orderDetail, isLoading } = useGetOrderDetail(id as string);
const { mutate: cancelOrder } = useCancelOrder();
const { id, name } = useParams();
const router = useRouter();
const queryClient = useQueryClient();
const { state: cancelModal, toggle: toggleCancelModal } = useToggle();
const { data: orderDetail, isLoading } = useGetOrderDetail(id as string);
const { mutate: cancelOrder } = useCancelOrder();
const onCancelOrder = (e: React.MouseEvent | null) => {
const onCancelOrder = (e: React.MouseEvent | null) => {
if (!e || !id) return;
if (!e || !id) return;
cancelOrder(id as string, {
onSuccess: () => {
toggleCancelModal();
queryClient.invalidateQueries({ queryKey: ["order-detail", id] });
toast("سفارش با موفقیت لغو شد", "success");
},
onError: (error) => {
toast(extractErrorMessage(error), "error");
},
});
};
cancelOrder(id as string, {
onSuccess: () => {
toggleCancelModal();
queryClient.invalidateQueries({ queryKey: ['order-detail', id] });
toast('سفارش با موفقیت لغو شد', 'success');
},
onError: (error) => {
toast(extractErrorMessage(error), 'error');
}
});
};
return (
<div className="fixed inset-0 bg-container flex flex-col">
<div className="flex-1 overflow-y-auto px-4 py-6">
<div className="max-w-2xl mx-auto flex flex-col gap-6">
{isLoading ? (
<>
<Skeleton className="h-6 w-48 mx-auto" />
<div className="flex flex-col gap-4">
<Skeleton className="h-5 w-full" />
<Skeleton className="h-5 w-full" />
<Skeleton className="h-20 w-full" />
<Skeleton className="h-5 w-full" />
</div>
</>
) : orderDetail?.data ? (
<>
<h1 className="text-lg font-semibold text-center">{getDeliveryMethodTitle(orderDetail.data.deliveryMethod.method)}</h1>
return (
<div className="fixed inset-0 bg-container flex flex-col">
<div className='flex-1 overflow-y-auto px-4 py-6'>
<div className='max-w-2xl mx-auto flex flex-col gap-6'>
{isLoading ? (
<>
<Skeleton className='h-6 w-48 mx-auto' />
<div className='flex flex-col gap-4'>
<Skeleton className='h-5 w-full' />
<Skeleton className='h-5 w-full' />
<Skeleton className='h-20 w-full' />
<Skeleton className='h-5 w-full' />
</div>
</>
) : orderDetail?.data ? (
<>
<h1 className='text-lg font-semibold text-center'>
{getDeliveryMethodTitle(orderDetail.data.deliveryMethod.method)}
</h1>
<div className="flex flex-col">
<div className="flex justify-between items-center h-16 border-b border-border">
<span className="text-sm text-muted-foreground flex items-center gap-2">
<Clock className="stroke-current" size={16} />
زمان سفارش
</span>
<div className="flex items-end gap-0.5">
<span className="text-sm font-semibold">{formatDate(orderDetail.data.createdAt)}</span>
{/* <span className='text-xs text-muted-foreground'>{formatTime(orderDetail.data.createdAt)}</span> */}
</div>
</div>
<div className="flex justify-between items-center h-16 border-b border-border">
<span className="text-sm text-muted-foreground">زمان آمادهسازی</span>
<span className="text-sm font-semibold">
{orderDetail.data.prepareTime
? `${orderDetail.data.prepareTime.toLocaleString("fa-IR")} دقیقه`
: "-"}
</span>
</div>
<div className="flex justify-between items-center h-16 border-b border-border">
<span className="text-sm text-muted-foreground">نوع ارسال</span>
<span className="text-sm font-semibold">{orderDetail.data?.deliveryMethod?.description}</span>
</div>
{orderDetail.data.carAddress && (
<div className="flex justify-between items-center h-16 border-b border-border">
<div className="text-sm text-muted-foreground mb-1">اطلاعات خودرو</div>
<div className="text-sm font-semibold">
{orderDetail.data.carAddress.carModel} {orderDetail.data.carAddress.carColor} - پلاک: {orderDetail.data.carAddress.plateNumber}
</div>
</div>
)}
<div className="flex justify-between items-center h-16 border-b border-border">
<span className="text-sm text-muted-foreground">شماره سفارش</span>
<span className="text-sm font-semibold">#{orderDetail.data.orderNumber}</span>
</div>
<div className="flex justify-between items-center h-16 border-b border-border">
<span className="text-sm text-muted-foreground">وضعیت سفارش</span>
<span className="text-sm font-semibold">{getStatusText(orderDetail.data.status)}</span>
</div>
<div className='flex flex-col'>
<div className='flex justify-between items-center h-16 border-b border-border'>
<span className='text-sm text-muted-foreground flex items-center gap-2'>
<Clock className='stroke-current' size={16} />
زمان سفارش
</span>
<div className='flex items-end gap-0.5'>
<span className='text-sm font-semibold'>{formatDate(orderDetail.data.createdAt)}</span>
{/* <span className='text-xs text-muted-foreground'>{formatTime(orderDetail.data.createdAt)}</span> */}
</div>
</div>
<div className='flex justify-between items-center h-16 border-b border-border'>
<span className='text-sm text-muted-foreground'>نوع ارسال</span>
<span className='text-sm font-semibold'>{orderDetail.data?.deliveryMethod?.description}</span>
</div>
{orderDetail.data.carAddress && (
<div className='flex justify-between items-center h-16 border-b border-border'>
<div className='text-sm text-muted-foreground mb-1'>اطلاعات خودرو</div>
<div className='text-sm font-semibold'>
{orderDetail.data.carAddress.carModel} {orderDetail.data.carAddress.carColor} - پلاک: {orderDetail.data.carAddress.plateNumber}
</div>
</div>
)}
<div className='flex justify-between items-center h-16 border-b border-border'>
<span className='text-sm text-muted-foreground'>شماره سفارش</span>
<span className='text-sm font-semibold'>#{orderDetail.data.orderNumber}</span>
</div>
<div className='flex justify-between items-center h-16 border-b border-border'>
<span className='text-sm text-muted-foreground'>وضعیت سفارش</span>
<span className='text-sm font-semibold'>{getStatusText(orderDetail.data.status)}</span>
</div>
{orderDetail.data.tableNumber && (
<div className="flex justify-between items-center h-16 border-b border-border">
<span className="text-sm text-muted-foreground">شماره میز</span>
<span className="text-sm font-semibold">#{orderDetail.data.tableNumber}</span>
</div>
)}
{orderDetail.data.tableNumber && (
<div className='flex justify-between items-center h-16 border-b border-border'>
<span className='text-sm text-muted-foreground'>شماره میز</span>
<span className='text-sm font-semibold'>#{orderDetail.data.tableNumber}</span>
</div>
)}
{orderDetail.data.userAddress && (
<div className="py-3 border-b border-border">
<div className="text-sm text-muted-foreground mb-1">آدرس تحویل</div>
<div className="text-sm">{orderDetail.data.userAddress.address}</div>
</div>
)}
{orderDetail.data.userAddress && (
<div className='py-3 border-b border-border'>
<div className='text-sm text-muted-foreground mb-1'>آدرس تحویل</div>
<div className='text-sm'>{orderDetail.data.userAddress.address}</div>
</div>
)}
{orderDetail.data.paymentMethod && (
<div className="flex justify-between items-center h-16 border-b border-border">
<span className="text-sm text-muted-foreground">نوع پرداخت</span>
<span className="text-sm font-semibold">{getPaymentMethodTitle(orderDetail.data.paymentMethod.method, orderDetail.data.paymentMethod.description)}</span>
</div>
)}
{orderDetail.data.paymentMethod && (
<div className='flex justify-between items-center h-16 border-b border-border'>
<span className='text-sm text-muted-foreground'>نوع پرداخت</span>
<span className='text-sm font-semibold'>
{getPaymentMethodTitle(
orderDetail.data.paymentMethod.method,
orderDetail.data.paymentMethod.description
)}
</span>
</div>
)}
{orderDetail.data.payments?.[0]?.description?.trim() && (
<div className="py-3 border-b border-border">
<div className="text-sm text-muted-foreground mb-1">توضیحات پرداخت</div>
<div className="text-sm">{orderDetail.data.payments[0].description}</div>
</div>
)}
{orderDetail.data.payments?.[0]?.description?.trim() && (
<div className='py-3 border-b border-border'>
<div className='text-sm text-muted-foreground mb-1'>توضیحات پرداخت</div>
<div className='text-sm'>{orderDetail.data.payments[0].description}</div>
</div>
)}
{orderDetail.data.payments?.[0]?.attachments?.length ? (
<div className='py-3 border-b border-border'>
<div className='text-sm text-muted-foreground mb-2'>پیوست پرداخت</div>
<div className='flex flex-col gap-2'>
{orderDetail.data.payments[0].attachments.map((attachment, index) => (
<div
key={attachment}
className='relative w-full h-40 rounded-normal overflow-hidden border border-border'
>
<Image
src={attachment}
alt={`پیوست پرداخت ${index + 1}`}
fill
className='object-contain'
/>
</div>
))}
</div>
</div>
) : null}
<div className='flex justify-between items-center py-4 border-t border-border'>
<span className='text-sm font-medium'>مجموع سفارش</span>
<span className='text-sm font-bold '>
{orderDetail.data.total.toLocaleString('fa-IR')} تومان
</span>
</div>
</div>
</>
) : (
<div className='text-center text-muted-foreground py-12'>
اطلاعات سفارش یافت نشد
{orderDetail.data.payments?.[0]?.attachments?.length ? (
<div className="py-3 border-b border-border">
<div className="text-sm text-muted-foreground mb-2">پیوست پرداخت</div>
<div className="flex flex-col gap-2">
{orderDetail.data.payments[0].attachments.map((attachment, index) => (
<div key={attachment} className="relative w-full h-40 rounded-normal overflow-hidden border border-border">
<Image src={attachment} alt={`پیوست پرداخت ${index + 1}`} fill className="object-contain" />
</div>
)}
</div>
</div>
))}
</div>
</div>
) : null}
<div className='p-4 border-t border-border bg-container'>
<div className='max-w-2xl mx-auto grid grid-cols-2 gap-4'>
<Button
className='dark:bg-white dark:text-black! dark:hover:bg-gray-100'
onClick={() => router.push(`/${name}`)}
type='button'>
بازگشت به منو
</Button>
<Button
type='submit'
onClick={toggleCancelModal}
className='bg-disabled! text-foreground!'
disabled={
orderDetail?.data?.status === 'cancelled' ||
orderDetail?.data?.status === 'delivered' ||
orderDetail?.data?.status === 'delivering'
}
>
لغو سفارش
</Button>
<div className="flex justify-between items-center py-4 border-t border-border">
<span className="text-sm font-medium">مجموع سفارش</span>
<span className="text-sm font-bold ">{orderDetail.data.total.toLocaleString("fa-IR")} تومان</span>
</div>
</div>
<div className={clsx(
'fixed inset-0 z-1001',
!cancelModal && 'pointer-events-none'
)}>
<Prompt
title={'لغو سفارش'}
description={'آیا از درخواست خود اطمینان دارید؟'}
textConfirm={'بله'}
textCancel={'منصرف شدم'}
onConfirm={onCancelOrder}
visible={cancelModal}
onClick={toggleCancelModal}
onCancel={toggleCancelModal}
/>
</div>
</div>
</>
) : (
<div className="text-center text-muted-foreground py-12">اطلاعات سفارش یافت نشد</div>
)}
</div>
);
</div>
<div className="p-4 border-t border-border bg-container">
<div className="max-w-2xl mx-auto grid grid-cols-2 gap-4">
<Button className="dark:bg-white dark:text-black! dark:hover:bg-gray-100" onClick={() => router.push(`/${name}`)} type="button">
بازگشت به منو
</Button>
<Button
type="submit"
onClick={toggleCancelModal}
className="bg-disabled! text-foreground!"
disabled={
orderDetail?.data?.status === 'cancelled' ||
orderDetail?.data?.status === "delivered" ||
orderDetail?.data?.status === "delivering"
}
>
لغو سفارش
</Button>
</div>
</div>
<div className={clsx("fixed inset-0 z-1001", !cancelModal && "pointer-events-none")}>
<Prompt
title={"لغو سفارش"}
description={"آیا از درخواست خود اطمینان دارید؟"}
textConfirm={"بله"}
textCancel={"منصرف شدم"}
onConfirm={onCancelOrder}
visible={cancelModal}
onClick={toggleCancelModal}
onCancel={toggleCancelModal}
/>
</div>
</div>
);
}
export default OrderTrackingPage;
export default OrderTrackingPage;
@@ -8,6 +8,7 @@ import React, { useEffect, useState } from 'react'
import { useVerify } from '../hooks/useVerifyData';
import { toast } from '@/components/Toast';
import { VerifyData } from '../types/Types';
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
type Props = object
@@ -74,7 +75,7 @@ export default function VerifyIndex({ }: Props) {
return (
<div className='h-full bg-inherit relative flex flex-col lg:gap-4'>
<div className='flex-1 flex flex-col items-center justify-center'>
<div className='bg-container py-5 px-2 rounded-container shadow-lg w-full'>
<div className={glassSurfaceCard('py-5 px-2 w-full')}>
<div className={`items-center gap-2 rounded-2xl h-35 flex flex-col justify-center ${isError ? 'bg-red-50' : 'bg-gray-50'}`}>
{isError ? (
<CloseCircle className='size-10 stroke-red-500' variant='TwoTone' />
@@ -0,0 +1,221 @@
"use client";
import ToggleButton, { ToggleButtonClassNames } from "@/components/button/ToggleButton";
import TabContainer, { TabContainerClassNames, TabContainerRenderType } from "@/components/tab/TabContainer";
import { TabHeader } from "@/components/tab/TabHeader";
import { toast, toastLoginRequired } from "@/components/Toast";
import RateSelectionBar from "@/components/utils/RateSelectionBar";
import ReviewChatThread from "./ReviewChatThread";
import { buildReviewThreads } from "../utils/reviewHelpers";
import { Button } from "@/components/ui/button";
import { getToken } from "@/lib/api/func";
import { extractErrorMessage } from "@/lib/func";
import { glassSurfaceFlat } from "@/lib/styles/glassSurface";
import clsx from "clsx";
import { useParams, useRouter } from "next/navigation";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { useGetProfile } from "../../../(Profile)/profile/hooks/userProfileData";
import { NegativePointEnum, PositivePointEnum } from "../enum/ReviewPoints";
import { useCreateReview, useGetFoodReviews } from "../hooks/useFoodData";
import { CreateReviewType } from "../types/ReviewTypes";
type Props = {
foodId: string;
};
const positivePointsArray = [
PositivePointEnum.GREAT_TASTE,
PositivePointEnum.FAST_DELIVERY,
PositivePointEnum.GOOD_QUALITY,
PositivePointEnum.GOOD_PORTION_SIZE,
PositivePointEnum.FRIENDLY_SERVICE,
];
const negativePointsArray = [
NegativePointEnum.SMALL_PORTION,
NegativePointEnum.SLOW_DELIVERY,
NegativePointEnum.POOR_QUALITY,
NegativePointEnum.OVERPRICED,
NegativePointEnum.UNFRIENDLY_SERVICE,
];
function FoodReviewsSection({ foodId }: Props) {
const { t } = useTranslation("rating");
const { name } = useParams();
const router = useRouter();
const { isSuccess } = useGetProfile();
const { data: reviewsData, isLoading: reviewsLoading } = useGetFoodReviews(foodId);
const { mutate: createReview, isPending } = useCreateReview(foodId);
const [rating, setRating] = useState<number>(5);
const [comment, setComment] = useState<string>("");
const [positivePoints, setPositivePoints] = useState<string[]>([]);
const [negativePoints, setNegativePoints] = useState<string[]>([]);
const [formKey, setFormKey] = useState(0);
const reviewThreads = buildReviewThreads(reviewsData?.data || []);
const getTranslationKey = (point: string, isPositive: boolean): string => {
const basePath = isPositive ? "Tabs.Strengths.Options" : "Tabs.Weeknesses.Options";
return `${basePath}.${point}`;
};
const handleToggle = (pointValue: string, isPositive: boolean) => (checked: boolean) => {
const setter = isPositive ? setPositivePoints : setNegativePoints;
if (checked) {
setter((prev) => [...prev, pointValue]);
} else {
setter((prev) => prev.filter((p) => p !== pointValue));
}
};
const handleRatingChange = (value: number[]) => {
setRating(value[0]);
};
const resetForm = () => {
setRating(5);
setComment("");
setPositivePoints([]);
setNegativePoints([]);
setFormKey((prev) => prev + 1);
};
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const token = await getToken();
if (!token || !isSuccess) {
toastLoginRequired(() => router.push(`/${name}/auth`), "error");
return;
}
if (rating === 0) {
toast(t("Errors.RatingRequired") || "لطفا امتیاز خود را انتخاب کنید", "error");
return;
}
const reviewData: CreateReviewType = {
foodId,
rating,
comment,
positivePoints,
negativePoints,
};
createReview(reviewData, {
onSuccess: () => {
toast(t("Success") || "نظر شما با موفقیت ثبت شد", "success");
resetForm();
},
onError: (error) => {
toast(extractErrorMessage(error, t("Errors.SubmitFailed") || "خطا در ثبت نظر"), "error");
},
});
};
const negativePointsView = (
<div className="grid grid-cols-2 gap-x-4.5 gap-y-4 mt-8">
{negativePointsArray.map((point) => (
<ToggleButton
className={{
...ToggleButtonClassNames,
wrapperActive: "border-red-400",
contentActive: "text-red-400",
}}
key={point}
name={`weekness_${point}`}
// @ts-expect-error - Type mismatch between custom ToggleButton and React types
onToggle={handleToggle(point, false)}
>
{t(getTranslationKey(point, false))}
</ToggleButton>
))}
</div>
);
const positivePointsView = (
<div className="grid grid-cols-2 gap-x-4.5 gap-y-4 mt-8">
{positivePointsArray.map((point) => (
<ToggleButton
key={point}
name={`strength_${point}`}
// @ts-expect-error - Type mismatch between custom ToggleButton and React types
onToggle={handleToggle(point, true)}
>
{t(getTranslationKey(point, true))}
</ToggleButton>
))}
</div>
);
return (
<section>
<h2 className="text-base font-bold">ثبت نظر</h2>
<form key={formKey} onSubmit={handleSubmit} className="mt-4">
<div className={glassSurfaceFlat("rounded-2xl p-4")}>
<RateSelectionBar name="rating" defaultValue={[rating]} onValueChange={handleRatingChange} />
<div className="mt-8">
<TabContainer
defaultIndex={1}
changeType={TabContainerRenderType.VISIBILITY}
className={{
...TabContainerClassNames,
wrapper: "p-2!",
scrollView: clsx(
"border-none! rounded-xl! gap-0! h-full bg-[#EAECF0]/80! dark:bg-neutral-900/80! not-dark:gradient-border! grid! grid-cols-2 px-2! py-2! pb-2! w-full overflow-y-hidden justify-center items-center",
),
title: "text-sm2! font-normal mt-1",
header: "rounded-lg h-7! w-full",
headerActive: "bg-white dark:bg-neutral-800",
titleActive: "text-primary",
}}
>
<TabHeader title={t("Tabs.Weeknesses.Title")} viewRenderer={negativePointsView} />
<TabHeader title={t("Tabs.Strengths.Title")} viewRenderer={positivePointsView} />
</TabContainer>
<div className="w-full mt-6">
<label htmlFor="foodReviewComment" className="text-sm font-medium">
{t("InputComment.Label")}
</label>
<textarea
name="foodReviewComment"
id="foodReviewComment"
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder={t("InputComment.Placeholder")}
className={glassSurfaceFlat(
"mt-2 w-full h-21 px-4 py-2.5 text-xs text-foreground rounded-xl border-0 focus:outline-none focus:ring-2 focus:ring-accent",
)}
/>
</div>
</div>
</div>
<Button type="submit" disabled={isPending} className="mt-4 w-full rounded-xl font-normal disabled:opacity-50 disabled:cursor-not-allowed">
{isPending ? t("ButtonSubmitting") || "در حال ارسال..." : t("ButtonSubmit")}
</Button>
</form>
<div className="mt-8 pt-6 border-t border-border/60">
<h2 className="text-sm2 font-medium leading-5">نظرات کاربران</h2>
<div>
{reviewsLoading ? (
<p className="text-sm2 text-center py-8 text-disabled-text">در حال بارگذاری نظرات...</p>
) : reviewThreads.length > 0 ? (
reviewThreads.map((thread) => <ReviewChatThread key={thread.root.id} thread={thread} />)
) : (
<p className="text-sm2 text-center py-8 text-disabled-text">هنوز نظری ثبت نشده است</p>
)}
</div>
</div>
</section>
);
}
export default FoodReviewsSection;
@@ -0,0 +1,98 @@
"use client";
import { Star1 } from "iconsax-react";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { ReviewThread, buildThreadMessages, formatReviewMessageTime } from "../utils/reviewHelpers";
type Props = {
thread: ReviewThread;
};
function ReviewChatThread({ thread }: Props) {
const { t } = useTranslation("rating");
const messages = useMemo(() => buildThreadMessages(thread), [thread]);
const getTranslationKey = (point: string, isPositive: boolean): string => {
const basePath = isPositive ? "Tabs.Strengths.Options" : "Tabs.Weeknesses.Options";
return `${basePath}.${point}`;
};
return (
<article className="py-4 border-b border-border/60 last:border-b-0">
<ul className="flex flex-col gap-4">
{messages.map((message) => {
const isAdmin = message.type === "admin";
return (
<li key={message.id} className={`flex w-full ${isAdmin ? "justify-start" : "justify-end"}`}>
<div
className={`w-full max-w-[min(420px,85%)] rounded-[24px] px-5 py-4 ${
isAdmin ? "bg-[#F6F7FA] dark:bg-gray-700 rounded-tr-none" : "bg-[#EBEDF5] dark:bg-gray-600 rounded-tl-none"
}`}
>
<h6 className="text-xs font-medium text-gray-700 dark:text-gray-200 mb-2">{message.senderName}</h6>
{message.parentMessage && (
<div className="mb-3 rounded-xl border-r-2 border-primary/50 bg-white/70 dark:bg-black/10 px-3 py-2">
<p className="text-[11px] font-medium text-gray-600 dark:text-gray-300">{message.parentMessage.senderName}</p>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1 line-clamp-3 whitespace-pre-wrap break-words">
{message.parentMessage.content}
</p>
</div>
)}
{!isAdmin && message.rating !== undefined && message.rating > 0 && (
<div className="flex items-center gap-1.5 mb-2">
<Star1 size={16} color="#FFB800" variant="Bold" />
<span className="text-sm font-medium">{message.rating}</span>
<span className="text-xs text-gray-500 dark:text-gray-400">از ۵</span>
</div>
)}
<p className="text-sm leading-7 text-gray-800 dark:text-gray-100 whitespace-pre-wrap break-words">{message.content}</p>
{!isAdmin && message.positivePoints && message.positivePoints.length > 0 && (
<div className="flex flex-wrap gap-2 mt-3">
{message.positivePoints.map((point, index) => (
<span
key={index}
className="px-3 py-1 border border-green-500 text-green-600 dark:text-green-400 rounded-xl text-xs bg-white dark:bg-transparent"
>
{t(getTranslationKey(point, true))}
</span>
))}
</div>
)}
{!isAdmin && message.negativePoints && message.negativePoints.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{message.negativePoints.map((point, index) => (
<span
key={index}
className="px-3 py-1 border border-red-500 text-red-600 dark:text-red-400 rounded-xl text-xs bg-white dark:bg-transparent"
>
{t(getTranslationKey(point, false))}
</span>
))}
</div>
)}
{message.createdAt && (
<div className="mt-3 flex justify-end">
<span dir="ltr" className="text-[11px] text-gray-400 dark:text-gray-500">
{formatReviewMessageTime(message.createdAt)}
</span>
</div>
)}
</div>
</li>
);
})}
</ul>
</article>
);
}
export default ReviewChatThread;
@@ -1,5 +1,6 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import * as api from "../service/Service";
import { CreateReviewType } from "../types/ReviewTypes";
export const useGetFood = (id: string) => {
return useQuery({
@@ -13,3 +14,22 @@ export const useToggleFavorite = () => {
mutationFn: api.toggleFavorite,
});
};
export const useGetFoodReviews = (foodId: string) => {
return useQuery({
queryKey: ["food-reviews", foodId],
queryFn: () => api.getFoodReviews(foodId),
enabled: !!foodId,
});
};
export const useCreateReview = (foodId: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (params: CreateReviewType) => api.createReview(params),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["food-reviews", foodId] });
},
});
};
+80 -77
View File
@@ -6,13 +6,15 @@ import PlusIcon from "@/components/icons/PlusIcon";
import { toast, toastLoginRequired } from "@/components/Toast";
import { getToken } from "@/lib/api/func";
import { ef } from "@/lib/helpers/utfNumbers";
import { glassSurface, glassSurfaceCard, glassSurfaceFlat } from "@/lib/styles/glassSurface";
import { motion } from "framer-motion";
import { ArrowLeft, Clock, Cup, Heart, TruckTick } from "iconsax-react";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useGetProfile } from "../../(Profile)/profile/hooks/userProfileData";
import { useGetAbout } from "../about/hooks/useAboutData";
import { useGetFood, useToggleFavorite } from "./hooks/useFoodData";
import FoodReviewsSection from "./components/FoodReviewsSection";
type Props = object;
@@ -25,7 +27,7 @@ function FoodPage({}: Props) {
const { items, addToCart, removeFromCart } = useCart();
const [isFavorite, setIsFavorite] = useState<boolean>(false);
const { mutate: toggleFavorite } = useToggleFavorite();
const imageRef = useRef<HTMLImageElement>(null);
const foodId = food?.data?.id || (id as string);
const quantity = useMemo(() => {
const item = items?.[foodId];
@@ -36,9 +38,8 @@ function FoodPage({}: Props) {
}, [items, foodId]);
const handleAddToCart = () => {
if (foodId) {
addToCart(foodId);
}
if (!foodId || food?.data?.isAvailable === false) return;
addToCart(foodId);
};
const handleRemoveFromCart = () => {
@@ -80,6 +81,39 @@ function FoodPage({}: Props) {
}
}, [food?.data?.isFavorite]);
useEffect(() => {
const scrollEl = document.querySelector("main");
const imageEl = imageRef.current;
if (!scrollEl || !imageEl) return;
const desktopQuery = window.matchMedia("(min-width: 1024px)");
const MAX_SCALE = 1.6;
const MIN_SCALE = 1.0;
const applyParallax = () => {
if (desktopQuery.matches) {
imageEl.style.transform = "";
return;
}
const scrollRange = imageEl.parentElement?.offsetHeight ?? 320;
const progress = Math.min(1, Math.max(0, scrollEl.scrollTop / scrollRange));
const scale = MIN_SCALE + (MAX_SCALE - MIN_SCALE) * progress;
imageEl.style.transform = `scale(${scale})`;
};
scrollEl.addEventListener("scroll", applyParallax, { passive: true });
desktopQuery.addEventListener("change", applyParallax);
window.addEventListener("resize", applyParallax);
applyParallax();
return () => {
scrollEl.removeEventListener("scroll", applyParallax);
desktopQuery.removeEventListener("change", applyParallax);
window.removeEventListener("resize", applyParallax);
imageEl.style.transform = "";
};
}, [food?.data?.id]);
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
@@ -97,6 +131,7 @@ function FoodPage({}: Props) {
}
const foodData = food.data;
const isAvailable = foodData.isAvailable !== false;
const foodName = foodData.title || foodData.name || foodData.foodName || "";
const foodImage =
typeof foodData.image === "string"
@@ -104,18 +139,14 @@ function FoodPage({}: Props) {
: Array.isArray(foodData.images) && foodData.images.length > 0
? typeof foodData.images[0] === "string"
? foodData.images[0]
: typeof foodData.images[0] === "object" &&
foodData.images[0] !== null &&
"url" in foodData.images[0]
: typeof foodData.images[0] === "object" && foodData.images[0] !== null && "url" in foodData.images[0]
? (foodData.images[0] as { url: string }).url
: "/assets/images/no-image.png"
: "/assets/images/no-image.png";
const categoryName = foodData.category?.title;
const prepareTime = foodData.prepareTime || 0;
const price = foodData.price || 0;
const content = Array.isArray(foodData.content)
? foodData.content.join("، ")
: foodData.content || foodData.desc || "";
const content = Array.isArray(foodData.content) ? foodData.content.join("، ") : foodData.content || foodData.desc || "";
const handleBackToMenu = () => {
const urlParams = new URLSearchParams(window.location.search);
@@ -128,46 +159,40 @@ function FoodPage({}: Props) {
};
return (
<div className="not-lg:max-w-lg not-lg:place-self-center not-lg:w-full not-lg:mb-10 pt-6 not-lg:flex not-lg:flex-col lg:fixed lg:z-10 lg:top-20 xl:top-22 lg:bottom-24 lg:left-5 lg:right-4 xl:right-[285px] lg:grid lg:grid-cols-2 lg:min-h-0 lg:overflow-hidden lg:rounded-2xl">
<div className="relative w-full lg:h-full min-h-0 not-lg:bg-container rounded-2xl overflow-hidden lg:rounded-r-none lg:order-1">
<div className="not-lg:max-w-lg not-lg:place-self-center not-lg:w-full w-full flex flex-col gap-4 pt-6 lg:pt-8 pb-24 lg:pb-28">
<div className="not-lg:flex not-lg:flex-col lg:grid lg:grid-cols-2 lg:min-h-[420px] lg:max-h-[480px] lg:rounded-2xl lg:overflow-hidden shrink-0">
<div
className={glassSurface(
"relative w-full lg:h-full min-h-0 rounded-2xl overflow-hidden lg:rounded-r-none lg:order-1 lg:bg-transparent! lg:border-0! lg:shadow-none! lg:[backdrop-filter:none] not-lg:sticky not-lg:top-6 not-lg:z-0 not-lg:h-[min(50vh,400px)] not-lg:min-h-[280px] not-lg:rounded-b-none",
)}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
className="lg:object-cover lg:h-full object-cover bg-[#F2F2F9] w-full"
ref={imageRef}
className="lg:object-cover not-lg:object-contain lg:h-full object-cover object-top bg-[#F2F2F9] w-full not-lg:absolute not-lg:inset-0 not-lg:h-full not-lg:origin-top not-lg:will-change-transform"
src={foodImage}
alt={foodName}
onError={(event) => {
event.currentTarget.src = "/assets/images/no-image.png";
}}
/>
<div className="absolute top-4 left-0 pe-5.5 ps-7 w-full inline-flex justify-between items-center">
<div className="bg-container whitespace-nowrap rounded-[34px] py-1.5 px-4 w-min text-xs text-disabled2 dark:text-disabled-text font-bold">
{categoryName || "-"}
</div>
<button
onClick={handleBackToMenu}
className="p-2 rounded-full bg-container/40"
>
<ArrowLeft
size={18}
className="stroke-primary dark:stroke-foreground"
/>
<div className="absolute top-4 left-0 z-10 pe-5.5 ps-7 w-full inline-flex justify-between items-center">
<div className={glassSurfaceFlat("whitespace-nowrap rounded-[34px] py-1.5 px-4 w-min text-xs text-disabled2 dark:text-disabled-text font-bold")}>{categoryName || "-"}</div>
<button onClick={handleBackToMenu} className={glassSurfaceFlat("p-2 rounded-full")}>
<ArrowLeft size={18} className="stroke-primary dark:stroke-foreground" />
</button>
</div>
</div>
<div className="relative px-6 pt-6 pb-7.5 lg:flex lg:flex-col lg:h-full lg:min-h-0 bg-container w-full rounded-3xl overflow-hidden not-lg:-translate-y-6 lg:rounded-l-none lg:overflow-y-auto">
<div
className={glassSurfaceCard(
"relative bg-white/60! px-6 pt-6 pb-7.5 lg:flex lg:flex-col lg:h-full lg:min-h-0 w-full overflow-hidden not-lg:z-10 not-lg:-mt-12 not-lg:rounded-t-3xl not-lg:shadow-[0_-10px_30px_rgba(0,0,0,0.08)] lg:rounded-l-none lg:overflow-y-auto rounded-3xl",
)}
>
<div className="w-full inline-flex justify-between items-center shrink-0">
<h5 className="text-base font-bold">{foodName}</h5>
<button
onClick={handleToggleFavorite}
className="p-2 bg-[#EAECF0] dark:bg-neutral-600 rounded-lg"
>
<Heart
variant={isFavorite ? "Bold" : "Outline"}
size={24}
color="currentColor"
className={isFavorite ? "fill-primary dark:fill-foreground" : ""}
/>
<button onClick={handleToggleFavorite} className="p-2 bg-[#EAECF0] dark:bg-neutral-600 rounded-lg">
<Heart variant={isFavorite ? "Bold" : "Outline"} size={24} color="currentColor" className={isFavorite ? "fill-primary dark:fill-foreground" : ""} />
</button>
</div>
@@ -175,10 +200,7 @@ function FoodPage({}: Props) {
<div className="text-xs">
<div className="flex items-center gap-1">
<Clock size={14} className="stroke-disabled-text" />
<span className="text-disabled-text">
زمان پخت و آماده سازی:{" "}
{prepareTime > 0 ? `${ef(String(prepareTime))} دقیقه` : "-"}
</span>
<span className="text-disabled-text">زمان پخت و آماده سازی: {prepareTime > 0 ? `${ef(String(prepareTime))} دقیقه` : "-"}</span>
</div>
<div className="flex items-center gap-2 mt-2">
<Cup size={14} className="stroke-disabled-text" />
@@ -188,18 +210,9 @@ function FoodPage({}: Props) {
<div>۰</div>
<span>امتیاز برای هر بار خرید</span>
</>
) : about?.data?.score?.purchaseScore &&
about?.data?.score?.purchaseAmount ? (
) : about?.data?.score?.purchaseScore && about?.data?.score?.purchaseAmount ? (
<>
<div>
{ef(
Math.round(
foodData?.price *
(Number(about.data.score.purchaseScore) /
Number(about.data.score.purchaseAmount)),
).toLocaleString("en-US"),
)}
</div>
<div>{ef(Math.round(foodData?.price * (Number(about.data.score.purchaseScore) / Number(about.data.score.purchaseAmount))).toLocaleString("en-US"))}</div>
<span>امتیاز برای هر بار خرید</span>
</>
) : (
@@ -209,9 +222,7 @@ function FoodPage({}: Props) {
</div>
<div className="flex items-center gap-2 mt-2">
<TruckTick size={14} className="stroke-disabled-text" />
<span className="text-disabled-text">
{foodData.pickupServe ? "ارسال با پیک" : "-"}
</span>
<span className="text-disabled-text">{foodData.pickupServe ? "ارسال با پیک" : "-"}</span>
</div>
</div>
@@ -224,40 +235,27 @@ function FoodPage({}: Props) {
<div className="mt-12 lg:mt-0 shrink-0 flex justify-between items-center">
<span dir="ltr">{ef(price.toLocaleString("en-US"))} T</span>
<motion.div
whileTap={{ scale: 1.05 }}
className="bg-background active:drop-shadow-xs max-w-[115px] rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2 relative overflow-hidden"
whileTap={isAvailable ? { scale: 1.05 } : undefined}
className="bg-background pattern-secondary-bg active:drop-shadow-xs max-w-[115px] rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2 relative overflow-hidden"
>
{quantity <= 0 ? (
<button
onClick={handleAddToCart}
className="inline-flex w-full justify-center items-center gap-2"
>
<PlusIcon />
<div className="text-sm2 pt-0.5 font-normal text-foreground">
افزودن
</div>
<button onClick={handleAddToCart} disabled={!isAvailable} className="inline-flex w-full justify-center items-center gap-2 disabled:pointer-events-none">
{isAvailable && <PlusIcon />}
<div className="text-sm2 pt-0.5 font-normal text-foreground">{isAvailable ? "افزودن" : "عدم موجودی"}</div>
</button>
) : (
<>
<button
onClick={handleAddToCart}
className="bg-container hover:bg-container/60 active:bg-container/30 rounded-sm p-2 transition-colors"
disabled={!isAvailable}
className={glassSurfaceFlat("hover:brightness-95 active:brightness-90 rounded-sm p-2 transition-all disabled:pointer-events-none")}
>
<PlusIcon className="text-foreground" />
</button>
<motion.div
key={quantity}
initial={{ scale: 1 }}
animate={{ scale: [1.2, 0.95, 1] }}
transition={{ duration: 0.3 }}
className="text-sm2 pt-0.5 font-semibold"
>
<motion.div key={quantity} initial={{ scale: 1 }} animate={{ scale: [1.2, 0.95, 1] }} transition={{ duration: 0.3 }} className="text-sm2 pt-0.5 font-semibold">
{quantity}
</motion.div>
<button
onClick={handleRemoveFromCart}
className="bg-container hover:bg-container/60 active:bg-container/30 rounded-sm p-2 transition-colors"
>
<button onClick={handleRemoveFromCart} className={glassSurfaceFlat("hover:brightness-95 active:brightness-90 rounded-sm p-2 transition-all")}>
<MinusIcon className="text-foreground" />
</button>
</>
@@ -265,6 +263,11 @@ function FoodPage({}: Props) {
</motion.div>
</div>
</div>
</div>
<div className={glassSurfaceCard("w-full px-6 pt-6 pb-7.5 rounded-3xl bg-white/60! shrink-0")}>
<FoodReviewsSection foodId={foodId} />
</div>
</div>
);
}
@@ -1,5 +1,6 @@
import { api } from "@/config/axios";
import { FoodResponse } from "../../types/Types";
import { CreateReviewType, FoodReviewsResponse } from "../types/ReviewTypes";
export const getFood = async (id: string): Promise<FoodResponse> => {
const { data } = await api.get<FoodResponse>(`/public/foods/${id}`);
@@ -10,3 +11,13 @@ export const toggleFavorite = async (foodId: string) => {
const { data } = await api.post(`/public/foods/favorite/${foodId}`);
return data;
};
export const getFoodReviews = async (foodId: string): Promise<FoodReviewsResponse> => {
const { data } = await api.get<FoodReviewsResponse>(`/public/reviews/${foodId}`);
return data;
};
export const createReview = async (params: CreateReviewType) => {
const { data } = await api.post("/public/reviews", params);
return data;
};
@@ -0,0 +1,12 @@
import { BaseResponse } from "../../types/Types";
import { Review } from "../../about/types/Types";
export type CreateReviewType = {
foodId: string;
rating: number;
comment: string;
positivePoints: string[];
negativePoints: string[];
};
export type FoodReviewsResponse = BaseResponse<Review[]>;
@@ -0,0 +1,140 @@
import { isReviewApproved, Review } from "../../about/types/Types";
export type ReviewThread = {
root: Review;
replies: Review[];
};
export function getParentId(parent: Review["parent"]): string | null {
if (!parent) return null;
if (typeof parent === "string") return parent;
if (typeof parent === "object" && "id" in parent) return parent.id;
return null;
}
export function isPopulatedReview(value: Review["parent"]): value is Review {
return Boolean(value && typeof value === "object" && "comment" in value);
}
export function getDisplayName(firstName?: string | null, lastName?: string | null, fallback = "کاربر"): string {
return [firstName, lastName].filter(Boolean).join(" ") || fallback;
}
export function buildReviewThreads(reviews: Review[]): ReviewThread[] {
const approved = reviews.filter(isReviewApproved);
const topLevel = approved.filter((review) => !getParentId(review.parent));
const topLevelById = new Map(topLevel.map((review) => [review.id, review]));
const repliesByParentId = new Map<string, Review[]>();
const orphanReplies: Review[] = [];
for (const review of approved) {
const parentId = getParentId(review.parent);
if (!parentId) continue;
if (topLevelById.has(parentId)) {
const list = repliesByParentId.get(parentId) ?? [];
list.push(review);
repliesByParentId.set(parentId, list);
} else {
orphanReplies.push(review);
}
}
const threads: ReviewThread[] = topLevel.map((root) => ({
root,
replies: (repliesByParentId.get(root.id) ?? []).sort(
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
),
}));
for (const reply of orphanReplies) {
if (isPopulatedReview(reply.parent)) {
threads.push({
root: reply.parent,
replies: [reply],
});
}
}
return threads.sort((a, b) => new Date(b.root.createdAt).getTime() - new Date(a.root.createdAt).getTime());
}
export function resolveParentReview(reply: Review, threadRoot: Review): Review | null {
if (isPopulatedReview(reply.parent)) {
return reply.parent;
}
const parentId = getParentId(reply.parent);
if (parentId && parentId === threadRoot.id) {
return threadRoot;
}
return null;
}
export function reviewToUserMessage(review: Review): ChatMessage {
return {
id: review.id,
type: "user",
senderName: getDisplayName(review.user?.firstName, review.user?.lastName),
content: review.comment || "نظری ثبت نشده است",
createdAt: review.createdAt,
rating: review.rating,
positivePoints: review.positivePoints,
negativePoints: review.negativePoints,
};
}
export type ChatMessage = {
id: string;
type: "user" | "admin";
senderName: string;
content: string;
createdAt?: string;
rating?: number;
positivePoints?: string[];
negativePoints?: string[];
parentMessage?: {
senderName: string;
content: string;
};
};
export function buildThreadMessages(thread: ReviewThread): ChatMessage[] {
const rootMessage = reviewToUserMessage(thread.root);
const items: ChatMessage[] = [rootMessage];
thread.replies.forEach((reply) => {
const parentReview = resolveParentReview(reply, thread.root);
const parentMessage = parentReview
? {
senderName: getDisplayName(parentReview.user?.firstName, parentReview.user?.lastName),
content: parentReview.comment || "-",
}
: undefined;
items.push({
id: reply.id,
type: "admin",
senderName: getDisplayName(reply.admin?.firstName, reply.admin?.lastName, "رستوران"),
content: reply.comment || "-",
createdAt: reply.createdAt,
parentMessage,
});
});
return items;
}
export function formatReviewMessageTime(date?: string) {
if (!date) return "";
return new Date(date).toLocaleString("fa-IR", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
}
+113 -147
View File
@@ -1,34 +1,32 @@
'use client'
import AnimatedBottomSheet from '@/components/bottomsheet/AnimatedBottomSheet';
import EqualizerIcon from '@/components/icons/EqualizerIcon';
import TelegramIcon from '@/components/icons/TelegramIcon';
import TabContainer from '@/components/tab/TabContainer';
import { TabHeader } from '@/components/tab/TabHeader';
import Comment from '@/components/utils/Comment';
import RateBar from '@/components/utils/RateBar';
import useToggle from '@/hooks/helpers/useToggle';
import { ArrowDown2, CallCalling, Clock, Gallery, InfoCircle, Instagram, Location, Star1, Whatsapp } from 'iconsax-react';
import { useQueryState } from 'next-usequerystate';
import Image from 'next/image';
import React from 'react'
import { useGetAbout, useGetReviews, useGetSchedules } from './hooks/useAboutData';
import AboutSkeleton from './components/AboutSkeleton';
"use client";
import AnimatedBottomSheet from "@/components/bottomsheet/AnimatedBottomSheet";
import EqualizerIcon from "@/components/icons/EqualizerIcon";
import TelegramIcon from "@/components/icons/TelegramIcon";
import TabContainer from "@/components/tab/TabContainer";
import { TabHeader } from "@/components/tab/TabHeader";
import Comment from "@/components/utils/Comment";
import RateBar from "@/components/utils/RateBar";
import useToggle from "@/hooks/helpers/useToggle";
import { glassSurfaceCard, glassSurfaceCardFlat, glassSurfaceFlat } from "@/lib/styles/glassSurface";
import { ArrowDown2, CallCalling, Clock, Gallery, InfoCircle, Instagram, Location, Star1, Whatsapp } from "iconsax-react";
import { useQueryState } from "next-usequerystate";
import Image from "next/image";
import AboutSkeleton from "./components/AboutSkeleton";
import { useGetAbout, useGetReviews, useGetSchedules } from "./hooks/useAboutData";
import { isReviewApproved } from "./types/Types";
const sortings = [
'جدیدترین',
'قدیمی ترین'
]
const sortings = ["جدیدترین", "قدیمی ترین"];
function AboutPage() {
const { data: aboutData, isLoading: aboutLoading } = useGetAbout();
const { data: reviewsData, isLoading: reviewsLoading } = useGetReviews();
const { data: schedulesData, isLoading: schedulesLoading } = useGetSchedules();
const [sorting, setSorting] = useQueryState('sortBy', { defaultValue: '0' });
const [sorting, setSorting] = useQueryState("sortBy", { defaultValue: "0" });
const { state: sortingModal, toggle: toggleSortingModal, set: setSortingModal } = useToggle();
const restaurant = aboutData?.data;
const schedules = schedulesData?.data || [];
const phoneList = Array.isArray(restaurant?.phones) ? restaurant.phones.filter((phone): phone is string => Boolean(phone)) : restaurant?.phone ? [restaurant.phone] : [];
const isLoading = aboutLoading || reviewsLoading || schedulesLoading;
@@ -39,22 +37,22 @@ function AboutPage() {
const changeSorting = (index: number) => {
setSorting(() => String(index));
setSortingModal(false);
}
};
const formatTime = (time: string): string => {
return time.substring(0, 5);
};
const getDayName = (weekDay: number): string => {
const days = ['یکشنبه', 'دوشنبه', 'سه‌شنبه', 'چهارشنبه', 'پنج‌شنبه', 'جمعه', 'شنبه'];
return days[weekDay] || '';
const days = ["یکشنبه", "دوشنبه", "سه‌شنبه", "چهارشنبه", "پنج‌شنبه", "جمعه", "شنبه"];
return days[weekDay] || "";
};
const groupSchedulesByDay = () => {
const grouped: Record<number, typeof schedules> = {};
schedules
.filter(schedule => schedule.isActive)
.forEach(schedule => {
.filter((schedule) => schedule.isActive)
.forEach((schedule) => {
if (!grouped[schedule.weekDay]) {
grouped[schedule.weekDay] = [];
}
@@ -65,9 +63,7 @@ function AboutPage() {
const getTodaySchedule = () => {
const today = new Date().getDay();
const todaySchedules = schedules.filter(schedule =>
schedule.isActive && schedule.weekDay === today
);
const todaySchedules = schedules.filter((schedule) => schedule.isActive && schedule.weekDay === today);
if (todaySchedules.length > 0) {
const firstSchedule = todaySchedules[0];
return `${formatTime(firstSchedule.openTime)} تا ${formatTime(firstSchedule.closeTime)}`;
@@ -79,102 +75,90 @@ function AboutPage() {
if (!restaurant) return null;
return (
<section aria-labelledby="about-title" className='py-4'>
<section
className="bg-container rounded-container shadow-container p-4">
<div className="flex justify-between items-center border-b-[1.5px] border-gray-200 pb-[25px]">
<section aria-labelledby="about-title" className="py-4">
<section className={glassSurfaceCard("p-4")}>
<div className="flex justify-between items-center border-b-[1.5px] border-gray-200 pb-6.25">
<div className="">
<h2 className='text-sm2 font-bold leading-5'>{restaurant.name}</h2>
{restaurant.establishedYear && (
<p className="text-sm2 leading-5 mt-4">تاسیس: {restaurant.establishedYear}</p>
)}
{restaurant.tagNames && restaurant.tagNames.length > 0 && (
<p className="text-sm2 leading-5 mt-2">نوع محصولات: {restaurant.tagNames.join('، ')}</p>
)}
<h2 className="text-sm2 font-bold leading-5">{restaurant.name}</h2>
{restaurant.establishedYear && <p className="text-sm2 leading-5 mt-4">تاسیس: {restaurant.establishedYear}</p>}
{restaurant.tagNames && restaurant.tagNames.length > 0 && <p className="text-sm2 leading-5 mt-2">نوع محصولات: {restaurant.tagNames.join("، ")}</p>}
</div>
{restaurant.logo && (
<div className="rounded-normal overflow-clip">
<Image
alt='logo'
src={restaurant.logo}
width={88}
height={88}
unoptimized
priority
/>
<Image alt="logo" src={restaurant.logo} width={88} height={88} unoptimized priority />
</div>
)}
</div>
{restaurant.description && (
<div className="mt-[23px]">
<div className="mt-5.75">
<h3 className="text-sm2 font-bold leading-5">درباره مجموعه</h3>
<p className="text-sm2 leading-5 mt-3 border-spacing-48" style={{ wordSpacing: '0.01em' }}>
<p className="text-sm2 leading-5 mt-3 border-spacing-48" style={{ wordSpacing: "0.01em" }}>
{restaurant.description}
</p>
{restaurant.images && restaurant.images.length > 0 && (
<div className='inline-flex gap-2 mt-[23px] items-center'>
<Gallery size={20} className='stroke-disabled-text' />
<span className='text-sm2 text-disabled-text font-medium pt-0.5'>عکس های رستوران</span>
<div className="inline-flex gap-2 mt-5.75 items-center">
<Gallery size={20} className="stroke-disabled-text" />
<span className="text-sm2 text-disabled-text font-medium pt-0.5">عکس های رستوران</span>
</div>
)}
</div>
)}
</section>
{(restaurant.phone || restaurant.telegram || restaurant.whatsapp || restaurant.instagram) && (
<section
className="bg-container rounded-container shadow-container pt-3 pb-3.5 px-[16px] mt-4 grid grid-cols-3 items-center">
<h2 className='text-sm2 font-medium leading-5'>ارتباط</h2>
<div className='col-span-1 text-center flex justify-center gap-4'>
{restaurant.phone &&
<a href={`tel://${restaurant.phone}`} className='bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal'>
<CallCalling className='stroke-foreground' size={24} />
{(phoneList.length > 0 || restaurant.telegram || restaurant.whatsapp || restaurant.instagram) && (
<section className={glassSurfaceCard("pt-3 pb-3.5 px-4 mt-4 grid grid-cols-3 items-center")}>
<h2 className="text-sm2 font-medium leading-5">ارتباط</h2>
<div className="col-span-2 text-center flex justify-center flex-wrap gap-2">
{phoneList.map((phone) => (
<a key={phone} href={`tel://${phone}`} className="bg-[#EAEDF5] dark:bg-neutral-700 px-2 py-1.5 rounded-normal inline-flex items-center gap-1.5">
<CallCalling className="stroke-foreground" size={24} />
<span dir="ltr" className="text-xs leading-5">
{phone}
</span>
</a>
}
{restaurant.telegram &&
<a href={`https://t.me/${restaurant.telegram}`} className='bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal'>
<TelegramIcon className='stroke-foreground' width={24} height={24} />
))}
{restaurant.telegram && (
<a href={`https://t.me/${restaurant.telegram}`} className="bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal">
<TelegramIcon className="stroke-foreground" width={24} height={24} />
</a>
}
{restaurant.whatsapp &&
<a href={`https://whatsapp.com/?phone=${restaurant.whatsapp}`} className='bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal'>
<Whatsapp className='stroke-foreground' size={24} />
)}
{restaurant.whatsapp && (
<a href={`https://whatsapp.com/?phone=${restaurant.whatsapp}`} className="bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal">
<Whatsapp className="stroke-foreground" size={24} />
</a>
}
{restaurant.instagram &&
<a href={`https://instagram.com/${restaurant.instagram}`} className='bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal'>
<Instagram className='stroke-foreground' size={24} />
)}
{restaurant.instagram && (
<a href={`https://instagram.com/${restaurant.instagram}`} className="bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal">
<Instagram className="stroke-foreground" size={24} />
</a>
}
)}
</div>
</section>
)}
{restaurant.address && (
<section className="bg-container rounded-container shadow-container px-4 pt-6 pb-6 mt-4">
<h2 className='text-sm2 font-medium leading-5'>آدرس</h2>
<p className='text-sm2 mt-[9px] leading-5'>{restaurant.address}</p>
<section className={glassSurfaceCard("px-4 pt-6 pb-6 mt-4")}>
<h2 className="text-sm2 font-medium leading-5">آدرس</h2>
<p className="text-sm2 mt-2.25 leading-5">{restaurant.address}</p>
{restaurant.latitude && restaurant.longitude && (
<div className='inline-flex gap-2 mt-[23px] items-center '>
<Location size={20} className='stroke-disabled-text' />
<span className='text-sm2 text-disabled-text '>موقعیت روی نقشه</span>
<div className="inline-flex gap-2 mt-5.75 items-center ">
<Location size={20} className="stroke-disabled-text" />
<span className="text-sm2 text-disabled-text ">موقعیت روی نقشه</span>
</div>
)}
</section>
)}
{schedules.length > 0 && getTodaySchedule() && (
<section
aria-label='ساعات کاری'
className="bg-container rounded-container shadow-container py-6 px-4 mt-4 flex justify-between items-center">
<section aria-label="ساعات کاری" className={glassSurfaceCard("py-6 px-4 mt-4 flex justify-between items-center")}>
<div className="flex items-center gap-2 justify-start">
<Clock size={16} className='stroke-disabled-text' />
<div className='text-sm2 font-medium leading-5 mt-0.5 text-[#8C90A3]'>باز</div>
<div className='size-1.5 bg-[#D9D9D9] rounded-full'></div>
<div className='text-sm2 mt-0.5'>امروز از ساعت {getTodaySchedule()}</div>
<Clock size={16} className="stroke-disabled-text" />
<div className="text-sm2 font-medium leading-5 mt-0.5 text-[#8C90A3]">باز</div>
<div className="size-1.5 bg-[#D9D9D9] rounded-full"></div>
<div className="text-sm2 mt-0.5">امروز از ساعت {getTodaySchedule()}</div>
</div>
<div className="">
<ArrowDown2 size={16} className='stroke-[#292D32]' />
<ArrowDown2 size={16} className="stroke-[#292D32]" />
</div>
</section>
)}
@@ -184,18 +168,13 @@ function AboutPage() {
{Object.entries(groupSchedulesByDay())
.sort(([a], [b]) => Number(a) - Number(b))
.map(([weekDay, daySchedules]) => (
<div
key={weekDay}
style={{ boxShadow: '0px 0px 14px 0px #0000000F' }}
className="bg-[#F6F6FA] dark:bg-border rounded-container leading-5 py-6 px-4 mt-4 flex justify-between items-center">
<div className="text-sm2">
{getDayName(Number(weekDay))}
</div>
<div className='text-sm2 font-light'>
<div key={weekDay} className={glassSurfaceCardFlat("leading-5 py-6 px-4 mt-4 flex justify-between items-center")}>
<div className="text-sm2">{getDayName(Number(weekDay))}</div>
<div className="text-sm2 font-light">
{daySchedules.map((schedule, index) => (
<span key={schedule.id}>
{formatTime(schedule.openTime)} - {formatTime(schedule.closeTime)}
{index < daySchedules.length - 1 && '، '}
{index < daySchedules.length - 1 && "، "}
</span>
))}
</div>
@@ -204,23 +183,23 @@ function AboutPage() {
</section>
)}
</section>
)
}
);
};
const formatDate = (dateString: string): string => {
const date = new Date(dateString);
return date.toLocaleDateString('fa-IR', {
year: 'numeric',
month: 'long',
day: 'numeric'
return date.toLocaleDateString("fa-IR", {
year: "numeric",
month: "long",
day: "numeric",
});
};
const secondTab = () => {
const reviews = (reviewsData?.data || []).filter(review => review.isApproved);
const reviews = (reviewsData?.data || []).filter(isReviewApproved);
const sortedReviews = [...reviews].sort((a, b) => {
if (sorting === '0') {
if (sorting === "0") {
// جدیدترین
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
} else {
@@ -229,16 +208,14 @@ function AboutPage() {
}
});
const averageRating = reviews.length > 0
? reviews.reduce((sum, review) => sum + review.rating, 0) / reviews.length
: 0;
const averageRating = reviews.length > 0 ? reviews.reduce((sum, review) => sum + review.rating, 0) / reviews.length : 0;
const ratingCounts = {
5: reviews.filter(r => r.rating === 5).length,
4: reviews.filter(r => r.rating === 4).length,
3: reviews.filter(r => r.rating === 3).length,
2: reviews.filter(r => r.rating === 2).length,
1: reviews.filter(r => r.rating === 1).length,
5: reviews.filter((r) => r.rating === 5).length,
4: reviews.filter((r) => r.rating === 4).length,
3: reviews.filter((r) => r.rating === 3).length,
2: reviews.filter((r) => r.rating === 2).length,
1: reviews.filter((r) => r.rating === 1).length,
};
const totalReviews = reviews.length;
@@ -251,27 +228,23 @@ function AboutPage() {
};
return (
<section aria-labelledby="reviews-title" className='py-4'>
<section
aria-label='امتیاز کاربران'
className="bg-container rounded-container shadow-container p-4 py-6 grid grid-cols-2 items-center">
<div className="text-center font-bold text-5xl">
{averageRating.toFixed(1)}
</div>
<section aria-labelledby="reviews-title" className="py-4">
<section aria-label="امتیاز کاربران" className={glassSurfaceCard("p-4 py-6 grid grid-cols-2 items-center")}>
<div className="text-center font-bold text-5xl">{averageRating.toFixed(1)}</div>
<div className="flex flex-col w-fit items-end">
<RateBar className='mt-1' content='5' percentage={String(ratingPercentages[5])} />
<RateBar className='mt-1' content='4' percentage={String(ratingPercentages[4])} />
<RateBar className='mt-1' content='3' percentage={String(ratingPercentages[3])} />
<RateBar className='mt-1' content='2' percentage={String(ratingPercentages[2])} />
<RateBar className='mt-1' content='1' percentage={String(ratingPercentages[1])} />
<RateBar className="mt-1" content="5" percentage={String(ratingPercentages[5])} />
<RateBar className="mt-1" content="4" percentage={String(ratingPercentages[4])} />
<RateBar className="mt-1" content="3" percentage={String(ratingPercentages[3])} />
<RateBar className="mt-1" content="2" percentage={String(ratingPercentages[2])} />
<RateBar className="mt-1" content="1" percentage={String(ratingPercentages[1])} />
</div>
</section>
<section className="bg-container rounded-container shadow-container pt-3 pb-3.5 px-[16px] mt-4">
<section className={glassSurfaceCard("pt-3 pb-3.5 px-4 mt-4")}>
<div className="flex justify-between items-center border-b-[1.5px] border-border pb-2">
<h2 className='text-sm2 font-medium leading-5'>نظرات کاربران</h2>
<button onClick={toggleSortingModal} className="rounded-xl h-8 bg-container ps-4 pe-2 py-1.5 inline-flex items-center justify-between gap-[7px]">
<EqualizerIcon className='dark:text-white' />
<h2 className="text-sm2 font-medium leading-5">نظرات کاربران</h2>
<button onClick={toggleSortingModal} className={glassSurfaceFlat("rounded-xl h-8 pattern-secondary-bg ps-4 pe-2 py-1.5 inline-flex items-center justify-between gap-1.75")}>
<EqualizerIcon className="dark:text-white" />
<span className="text-xs leading-5 font-medium">{sortings[+sorting]}</span>
</button>
</div>
@@ -280,8 +253,8 @@ function AboutPage() {
sortedReviews.map((review) => (
<article key={review.id}>
<Comment
className='pt-8'
user={review.user?.firstName + ' ' + review.user?.lastName}
className="pt-8"
user={review.user?.firstName + " " + review.user?.lastName}
rating={review.rating}
date={formatDate(review.createdAt)}
text={review.comment}
@@ -292,7 +265,7 @@ function AboutPage() {
</article>
))
) : (
<p className='text-sm2 text-center py-8 text-disabled-text'>هنوز نظری ثبت نشده است</p>
<p className="text-sm2 text-center py-8 text-disabled-text">هنوز نظری ثبت نشده است</p>
)}
</div>
</section>
@@ -306,29 +279,22 @@ function AboutPage() {
</div>
{i < sortings.length - 1 && <hr className="border-white/40 dark:border-border mb-4 mt-4" />}
</div>
)
);
})}
</div>
</AnimatedBottomSheet>
</section>
)
}
);
};
return (
<div className='pt-8 h-full overflow-y-auto noscrollbar'>
<div className="pt-8 h-full overflow-y-auto noscrollbar">
<TabContainer>
<TabHeader
viewRenderer={firstTab()}
title='درباره ما' icon={<InfoCircle size={24} />}>
</TabHeader>
<TabHeader
viewRenderer={secondTab()}
title='امتیازات و نظرات' icon={<Star1 size={24} />}>
</TabHeader>
<TabHeader viewRenderer={firstTab()} title="درباره ما" icon={<InfoCircle size={24} />}></TabHeader>
<TabHeader viewRenderer={secondTab()} title="امتیازات و نظرات" icon={<Star1 size={24} />}></TabHeader>
</TabContainer>
</div>
)
);
}
export default AboutPage
export default AboutPage;
@@ -4,12 +4,13 @@ import { Skeleton } from "@/components/ui/skeleton";
import TabContainer from "@/components/tab/TabContainer";
import { TabHeader } from "@/components/tab/TabHeader";
import { InfoCircle, Star1 } from 'iconsax-react';
import { glassSurfaceCard, glassSurfaceCardFlat } from '@/lib/styles/glassSurface';
const AboutSkeleton = () => {
const firstTabSkeleton = () => (
<section className='py-4'>
{/* اطلاعات رستوران */}
<section className="bg-container rounded-container shadow-container p-4">
<section className={glassSurfaceCard('p-4')}>
<div className="flex justify-between items-center border-b-[1.5px] border-gray-200 pb-[25px]">
<div className="flex-1">
<Skeleton className="h-6 w-32 rounded-md mb-4" />
@@ -31,7 +32,7 @@ const AboutSkeleton = () => {
</section>
{/* ارتباط */}
<section className="bg-container rounded-container shadow-container pt-3 pb-3.5 px-[16px] mt-4 grid grid-cols-3 items-center">
<section className={glassSurfaceCard('pt-3 pb-3.5 px-[16px] mt-4 grid grid-cols-3 items-center')}>
<Skeleton className="h-5 w-16 rounded-md" />
<div className='col-span-1 text-center flex justify-center gap-4'>
{[...Array(3)].map((_, index) => (
@@ -41,7 +42,7 @@ const AboutSkeleton = () => {
</section>
{/* آدرس */}
<section className="bg-container rounded-container shadow-container px-4 pt-6 pb-6 mt-4">
<section className={glassSurfaceCard('px-4 pt-6 pb-6 mt-4')}>
<Skeleton className="h-5 w-12 rounded-md mb-[9px]" />
<Skeleton className="h-4 w-full rounded-md mb-2" />
<Skeleton className="h-4 w-3/4 rounded-md mb-[23px]" />
@@ -52,7 +53,7 @@ const AboutSkeleton = () => {
</section>
{/* ساعات کاری امروز */}
<section className="bg-container rounded-container shadow-container py-6 px-4 mt-4 flex justify-between items-center">
<section className={glassSurfaceCard('py-6 px-4 mt-4 flex justify-between items-center')}>
<div className="flex items-center gap-2">
<Skeleton className="w-4 h-4 rounded-full" />
<Skeleton className="h-5 w-8 rounded-md" />
@@ -66,7 +67,7 @@ const AboutSkeleton = () => {
{[...Array(3)].map((_, index) => (
<div
key={index}
className="bg-[#F6F6FA] dark:bg-border rounded-container leading-5 py-6 px-4 mt-4 flex justify-between items-center">
className={glassSurfaceCardFlat('leading-5 py-6 px-4 mt-4 flex justify-between items-center')}>
<Skeleton className="h-5 w-20 rounded-md" />
<Skeleton className="h-5 w-24 rounded-md" />
</div>
@@ -77,7 +78,7 @@ const AboutSkeleton = () => {
const secondTabSkeleton = () => (
<section className='py-4'>
{/* امتیاز کاربران */}
<section className="bg-container rounded-container shadow-container p-4 py-6 grid grid-cols-2 items-center">
<section className={glassSurfaceCard('p-4 py-6 grid grid-cols-2 items-center')}>
<Skeleton className="h-16 w-20 rounded-md mx-auto" />
<div className="flex flex-col w-fit items-end gap-1">
{[...Array(5)].map((_, index) => (
@@ -87,7 +88,7 @@ const AboutSkeleton = () => {
</section>
{/* نظرات کاربران */}
<section className="bg-container rounded-container shadow-container pt-3 pb-3.5 px-[16px] mt-4">
<section className={glassSurfaceCard('pt-3 pb-3.5 px-[16px] mt-4')}>
<div className="flex justify-between items-center border-b-[1.5px] border-border pb-2">
<Skeleton className="h-5 w-24 rounded-md" />
<Skeleton className="h-8 w-28 rounded-xl" />
@@ -1,17 +1,37 @@
import { useQuery } from "@tanstack/react-query";
import * as api from "../service/AboutService";
import { useParams } from "next/navigation";
import { useEffect } from "react";
import * as api from "../service/AboutService";
import {
getPersistedAboutData,
setPersistedAboutData,
} from "@/lib/helpers/themeCache";
export const useGetAbout = () => {
const { name } = useParams<{ name: string }>();
return useQuery({
const query = useQuery({
queryKey: ["about", name],
queryFn: () => api.getAbout(name),
queryFn: async () => {
const data = await api.getAbout(name);
setPersistedAboutData(name, data);
return data;
},
enabled: !!name,
retry: false,
staleTime: 5 * 60_000,
refetchOnMount: false,
staleTime: 60 * 60_000,
gcTime: 24 * 60 * 60_000,
refetchOnMount: "always",
refetchOnWindowFocus: false,
placeholderData: () => (name ? getPersistedAboutData(name) : undefined),
});
useEffect(() => {
if (name && query.data) {
setPersistedAboutData(name, query.data);
}
}, [name, query.data]);
return query;
};
export const useGetReviews = () => {
+73 -42
View File
@@ -32,7 +32,8 @@ export interface Restaurant {
isActive: boolean;
establishedYear: number | null;
phoneNumber: string | null;
phone: string;
phone?: string | null;
phones?: string[];
instagram: string | null;
telegram: string | null;
whatsapp: string | null;
@@ -44,55 +45,73 @@ export interface Restaurant {
vat: number;
domain: string;
score: Score;
plan: 'base' | 'premium';
plan: "base" | "premium";
enableAiChat?: boolean;
bgType?: "pattern" | "custom" | "color";
bgBlur?: number;
bgOpacity?: number;
bgOverlay?: string;
bgUrl?: string;
}
export interface ReviewFood {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
restaurant: string;
category: string;
createdAt?: string;
updatedAt?: string;
deletedAt?: string | null;
restaurant?: string;
category?: string;
title: string;
desc: string | null;
content: string | null;
price: number;
points: number | null;
order: number | null;
prepareTime: number | null;
sat: boolean;
sun: boolean;
mon: boolean;
breakfast: boolean;
noon: boolean;
dinner: boolean;
stock: number;
stockDefault: number;
isActive: boolean;
images: string[];
inPlaceServe: boolean;
pickupServe: boolean;
rate: number;
discount: number;
isSpecialOffer: boolean;
desc?: string | null;
content?: string[] | string | null;
price?: number;
points?: number | null;
order?: number | null;
prepareTime?: number | null;
weekDays?: number[];
mealTypes?: string[];
sat?: boolean;
sun?: boolean;
mon?: boolean;
breakfast?: boolean;
noon?: boolean;
dinner?: boolean;
stock?: number;
stockDefault?: number;
isActive?: boolean;
images?: string[];
inPlaceServe?: boolean;
pickupServe?: boolean;
rate?: number;
score?: number;
discount?: number;
isSpecialOffer?: boolean;
}
export interface ReviewUser {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
createdAt?: string;
updatedAt?: string;
deletedAt?: string | null;
firstName: string;
lastName: string;
birthDate: string;
marriageDate: string;
referrer: string | null;
isActive: boolean;
gender: boolean;
wallet: number;
points: number;
phone: string;
birthDate?: string;
marriageDate?: string;
referrer?: string | null;
isActive?: boolean;
gender?: boolean;
wallet?: number;
points?: number;
phone?: string;
avatarUrl?: string | null;
}
export type ReviewStatus = "approved" | "pending" | "rejected";
export interface ReviewAdmin {
id: string;
firstName?: string | null;
lastName?: string | null;
}
export interface Review {
@@ -100,14 +119,26 @@ export interface Review {
createdAt: string;
updatedAt: string;
deletedAt: string | null;
order: string;
isBuyer?: boolean;
order?: string;
food: ReviewFood;
user: ReviewUser;
user?: ReviewUser | null;
admin?: ReviewAdmin | null;
comment: string;
rating: number;
positivePoints: string[];
negativePoints: string[];
isApproved: boolean;
status?: ReviewStatus;
parent?: string | { id: string } | Review | null;
/** @deprecated use status */
isApproved?: boolean;
}
export function isReviewApproved(review: Review): boolean {
if (review.status) {
return review.status === "approved";
}
return review.isApproved === true;
}
export interface Schedule {
+201
View File
@@ -0,0 +1,201 @@
"use client";
import { useGetAbout } from "@/app/[name]/(Main)/about/hooks/useAboutData";
import type { Food } from "@/app/[name]/(Main)/types/Types";
import { useGetProfile } from "@/app/[name]/(Profile)/profile/hooks/userProfileData";
import MenuItem from "@/components/listview/MenuItem";
import MenuItemRenderer from "@/components/listview/MenuItemRenderer";
import { toast } from "@/components/Toast";
import { extractErrorMessage } from "@/lib/func";
import { glassSurfaceFlat } from "@/lib/styles/glassSurface";
import { ArrowLeft, Send2 } from "iconsax-react";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { FormEvent, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useChat } from "./hooks/useAiData";
import { AI_SUGGESTIONS } from "./suggestions";
import type { ChatFood } from "./types/Types";
type Message = {
id: string;
role: "user" | "assistant";
content: string;
foods?: ChatFood[];
};
function getGreetingKey() {
const hour = new Date().getHours();
if (hour >= 5 && hour < 12) return "morning";
if (hour >= 12 && hour < 14) return "noon";
if (hour >= 14 && hour < 17) return "afternoon";
return "evening";
}
function toFood(food: ChatFood): Food {
return {
id: food.id,
category: "",
title: food.title,
desc: food.desc,
content: food.content ?? [],
price: food.price,
prepareTime: 0,
isActive: true,
images: food.images ?? [],
inPlaceServe: true,
pickupServe: true,
discount: food.discount ?? 0,
isSpecialOffer: food.isSpecialOffer ?? false,
isAvailable: true,
unavailabilityMessage: null,
};
}
function AiPage() {
const { t } = useTranslation("common", { keyPrefix: "AI" });
const router = useRouter();
const { data: about } = useGetAbout();
const { data: profile, isSuccess } = useGetProfile();
const { mutate: chat, isPending } = useChat();
const [input, setInput] = useState("");
const [messages, setMessages] = useState<Message[]>([]);
const listRef = useRef<HTMLUListElement>(null);
const restaurant = about?.data;
const brandName = restaurant?.name || t("BrandFallback");
const userName = isSuccess && profile?.data?.firstName ? profile.data.firstName : t("GuestName");
useEffect(() => {
listRef.current?.scrollTo({ top: listRef.current.scrollHeight, behavior: "smooth" });
}, [messages, isPending]);
const sendMessage = (text: string) => {
const content = text.trim();
if (!content || isPending) return;
const userMessage: Message = { id: `u-${Date.now()}`, role: "user", content };
setMessages((prev) => [...prev, userMessage]);
setInput("");
chat(
{ question: content },
{
onSuccess: (res) => {
setMessages((prev) => [
...prev,
{
id: `a-${Date.now()}`,
role: "assistant",
content: res.data.answer,
foods: res.data.foods?.length ? res.data.foods : undefined,
},
]);
},
onError: (error) => {
toast(extractErrorMessage(error, t("ErrorFallback")), "error");
},
},
);
};
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
sendMessage(input);
};
return (
<section className="flex h-[calc(100svh-6.5rem)] flex-col pt-4 xl:h-[calc(100svh-6rem)]">
<div className="grid shrink-0 grid-cols-3 items-center">
<span />
<h1 className="place-self-center text-sm2 font-medium">{t("Heading")}</h1>
<ArrowLeft className="cursor-pointer place-self-end" size="20" color="currentColor" onClick={() => router.back()} />
</div>
<div className="mt-6 flex min-h-0 flex-1 flex-col">
{messages.length > 0 ? (
<ul ref={listRef} className="noscrollbar flex-1 space-y-3 overflow-y-auto pb-4">
{messages.map((message) => {
const isUser = message.role === "user";
const hasFoods = !isUser && !!message.foods?.length;
return (
<li key={message.id} className={`flex flex-col gap-2 ${isUser ? "items-end" : "items-start"}`}>
<div
className={
isUser
? "max-w-[80%] rounded-[20px] rounded-br-none bg-primary/15 px-4 py-3 dark:bg-primary/25"
: "max-w-[80%] rounded-[20px] rounded-bl-none bg-[#EBEDF5] px-4 py-3 dark:bg-gray-600"
}
>
<p className="text-xs2 leading-5 whitespace-pre-wrap">{message.content}</p>
</div>
{hasFoods && (
<div className="flex w-full flex-col gap-2">
{message.foods!.map((food) => (
<MenuItemRenderer key={food.id}>
<MenuItem food={toFood(food)} />
</MenuItemRenderer>
))}
</div>
)}
</li>
);
})}
{isPending && (
<li className="flex justify-start">
<div className="max-w-[80%] rounded-[20px] rounded-bl-none bg-[#EBEDF5] px-4 py-3 dark:bg-gray-600">
<p className="animate-pulse text-xs2 leading-5 text-disabled-text">{t("Thinking")}</p>
</div>
</li>
)}
</ul>
) : (
<div className="flex flex-1 flex-col items-center justify-center gap-8">
<div className="flex flex-col items-center gap-2">
<Image src={"/assets/images/logo_ai.svg"} alt={brandName} width={299} height={299} className="w-74.75 h-auto" unoptimized />
</div>
<div className="flex w-full flex-col items-center gap-4">
<h2 className="text-base font-medium">{t(`Greeting.${getGreetingKey()}`, { name: userName })}</h2>
<div className="flex w-full flex-wrap gap-2">
{AI_SUGGESTIONS.map(({ labelKey, icon: Icon }) => (
<button
key={labelKey}
type="button"
disabled={isPending}
onClick={() => sendMessage(t(labelKey))}
className={glassSurfaceFlat("inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 transition-[filter] active:brightness-95 disabled:opacity-50")}
>
<Icon size={15} className="stroke-foreground" variant="Bold" />
<span className="text-xs2">{t(labelKey)}</span>
</button>
))}
</div>
</div>
</div>
)}
<form onSubmit={onSubmit} className={glassSurfaceFlat("mt-3 flex h-12 shrink-0 items-center gap-1 rounded-xl border border-border px-3")}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder={t("InputPlaceholder")}
autoComplete="off"
disabled={isPending}
className="h-full w-full bg-transparent text-[11px]! outline-none placeholder:text-disabled-text disabled:opacity-60"
/>
<button type="submit" disabled={!input.trim() || isPending} className="rounded-full p-1.5 disabled:opacity-40">
<Send2 size={20} variant="Bold" className="fill-primary text-primary dark:fill-foreground dark:text-foreground" />
</button>
</form>
</div>
</section>
);
}
export default AiPage;
@@ -0,0 +1,8 @@
import { useMutation } from "@tanstack/react-query";
import * as api from "../service/AiService";
export const useChat = () => {
return useMutation({
mutationFn: api.chat,
});
};
+3
View File
@@ -0,0 +1,3 @@
'use client'
export { default } from './AiPage'
@@ -0,0 +1,7 @@
import { api } from "@/config/axios";
import { ChatRequest, ChatResponse } from "../types/Types";
export const chat = async (params: ChatRequest) => {
const { data } = await api.post<ChatResponse>("/public/ai/chat", params);
return data;
};
+29
View File
@@ -0,0 +1,29 @@
import type { Icon } from 'iconsax-react'
import {
Apple,
Cake,
Chart,
FavoriteChart,
Flash,
Health,
LikeTag,
People,
Timer,
} from 'iconsax-react'
export type Suggestion = {
labelKey: string
icon: Icon
}
export const AI_SUGGESTIONS: Suggestion[] = [
{ labelKey: 'Suggestions.vegetarian', icon: Apple },
{ labelKey: 'Suggestions.healthy', icon: Health },
{ labelKey: 'Suggestions.tasty', icon: LikeTag },
{ labelKey: 'Suggestions.protein', icon: FavoriteChart },
{ labelKey: 'Suggestions.bestsellers', icon: Chart },
{ labelKey: 'Suggestions.spicy', icon: Flash },
{ labelKey: 'Suggestions.quick', icon: Timer },
{ labelKey: 'Suggestions.dessert', icon: Cake },
{ labelKey: 'Suggestions.group', icon: People },
]
+27
View File
@@ -0,0 +1,27 @@
import { BaseResponse } from "@/app/[name]/(Main)/types/Types";
export type ChatRequest = {
question: string;
};
export type ChatFood = {
id: string;
title: string;
desc: string;
content: string[] | null;
price: number;
images: string[];
discount: number;
isSpecialOffer: boolean;
};
export type ChatData = {
answer: string;
foods?: ChatFood[];
consumedTokens: number;
cost: number;
promptTokens: number;
completionTokens: number;
};
export type ChatResponse = BaseResponse<ChatData>;
+3 -2
View File
@@ -16,6 +16,7 @@ import CartSkeleton from "./components/CartSkeleton";
import CartSummary from "./components/CartSummary";
import { useCart } from "./hook/useCart";
import { useCartPageState } from "./hooks/useCartPageState";
import { glassSurfaceCard } from "@/lib/styles/glassSurface";
const CartPage = () => {
const { t } = useTranslation("parallels", { keyPrefix: "Cart" });
@@ -42,7 +43,7 @@ const CartPage = () => {
};
return (
<div className="overflow-y-auto h-full noscrollbar flex flex-col bg-background">
<div className="overflow-y-auto h-full noscrollbar flex flex-col">
<div className="grid grid-cols-3 items-center mt-6">
{isCartEmpty || isLoading ? (
<span></span>
@@ -93,7 +94,7 @@ const CartPage = () => {
) : (
<div className="flex flex-col gap-4 pb-24">
{!isPremium && (
<div className="bg-container rounded-container p-4 border border-border shadow-sm">
<div className={glassSurfaceCard('p-4')}>
<div className="flex items-start gap-3 mb-4">
<div className="shrink-0 mt-0.5">
<NotificationBellIcon
@@ -1,16 +1,17 @@
'use client';
"use client";
import React from 'react';
import Link from 'next/link';
import { useParams, useRouter, usePathname } from 'next/navigation';
import Button from '@/components/button/PrimaryButton';
import { useGetFoods } from '@/app/[name]/(Main)/hooks/useMenuData';
import { useCart } from '../hook/useCart';
import { useTranslation } from 'react-i18next';
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
import { useGetCartItems } from '../hooks/useCartData';
import { useCartStore } from '../store/Store';
import { guestCartItemsToFoods } from '../lib/cartUtils';
import { useGetFoods } from "@/app/[name]/(Main)/hooks/useMenuData";
import { useGetProfile } from "@/app/[name]/(Profile)/profile/hooks/userProfileData";
import Button from "@/components/button/PrimaryButton";
import { glassSurfaceNav } from "@/lib/styles/glassSurface";
import Link from "next/link";
import { useParams, usePathname, useRouter } from "next/navigation";
import React from "react";
import { useTranslation } from "react-i18next";
import { useCart } from "../hook/useCart";
import { useGetCartItems } from "../hooks/useCartData";
import { guestCartItemsToFoods } from "../lib/cartUtils";
import { useCartStore } from "../store/Store";
interface CartSummaryProps {
isPremium: boolean;
@@ -26,7 +27,7 @@ const CartSummary = ({ isPremium, hasItems }: CartSummaryProps) => {
const { data: foodsResponse } = useGetFoods();
const { items } = useCart();
const { data: cartData } = useGetCartItems(isSuccess);
const { t } = useTranslation('parallels', { keyPrefix: 'Cart' });
const { t } = useTranslation("parallels", { keyPrefix: "Cart" });
const { description, setDescription } = useCartStore();
const totalPrice = React.useMemo(() => {
@@ -45,10 +46,7 @@ const CartSummary = ({ isPremium, hasItems }: CartSummaryProps) => {
}, 0);
}, [isSuccess, cartData?.data?.total, foodsResponse?.data, items]);
const formatPrice = React.useCallback(
(value: number) => value.toLocaleString('fa-IR'),
[]
);
const formatPrice = React.useCallback((value: number) => value.toLocaleString("fa-IR"), []);
if (!hasItems) {
return null;
@@ -57,36 +55,32 @@ const CartSummary = ({ isPremium, hasItems }: CartSummaryProps) => {
return (
<>
{isPremium && (
<div className='mt-2 mb-4'>
<div className='relative'>
<h3 className='text-sm'>{t('InputDescription.Label')}</h3>
<div className="mt-2 mb-4">
<div className="relative">
<h3 className="text-sm">{t("InputDescription.Label")}</h3>
<textarea
className='w-full px-4 py-2.5 mt-4 text-sm2 leading-6 outline-0 border border-border rounded-normal resize-none focus:ring-0'
placeholder={t('InputDescription.Placeholder')}
id='description'
name='description'
className="w-full px-4 py-2.5 mt-4 text-sm2 leading-6 outline-0 border border-white rounded-normal bg-white/50 resize-none focus:ring-0"
placeholder={t("InputDescription.Placeholder")}
id="description"
name="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
></textarea>
<span className='absolute top-4 right-2 px-2 bg-background text-foreground text-xs'></span>
<span className="absolute top-4 right-2 px-2 bg-background text-foreground text-xs"></span>
</div>
</div>
)}
<div className='fixed bottom-0 left-0 right-0 z-50 bg-white dark:bg-container border-t border-border p-4 w-full'>
<div className='flex justify-between items-center gap-4'>
<div className='flex flex-col'>
<div className='text-xs text-gray-400 dark:text-disabled-text'>{t('PayableAmountLabel')}</div>
{isPremium && (
<div className='text-sm mt-2 font-semibold dark:text-foreground'>
{formatPrice(totalPrice)} تومان
</div>
)}
<div className={glassSurfaceNav("fixed bottom-0 left-0 right-0 z-50 border-t border-border p-4 w-full")}>
<div className="flex justify-between items-center gap-4">
<div className="flex flex-col">
<div className="text-xs text-gray-400 dark:text-disabled-text">{t("PayableAmountLabel")}</div>
{isPremium && <div className="text-sm mt-2 font-semibold dark:text-foreground">{formatPrice(totalPrice)} تومان</div>}
</div>
{isPremium && (
<Link
href={'order/checkout/0'}
href={"order/checkout/0"}
onClick={(event) => {
if (!isSuccess) {
event.preventDefault();
@@ -96,16 +90,10 @@ const CartSummary = ({ isPremium, hasItems }: CartSummaryProps) => {
}
}}
>
<Button className='w-fit! px-10 dark:bg-white dark:text-black! dark:hover:bg-gray-100'>
{t('ButtonCheckout')}
</Button>
<Button className="w-fit! px-10 dark:bg-white dark:text-black! dark:hover:bg-gray-100">{t("ButtonCheckout")}</Button>
</Link>
)}
{!isPremium && (
<div className='text-sm mt-2 font-semibold dark:text-foreground'>
{formatPrice(totalPrice)} تومان
</div>
)}
{!isPremium && <div className="text-sm mt-2 font-semibold dark:text-foreground">{formatPrice(totalPrice)} تومان</div>}
</div>
</div>
</>
@@ -20,6 +20,8 @@ export function cartItemToFood(item: CartItem, menuFood?: Food): Food {
pickupServe: true,
discount: item.discount,
isSpecialOffer: false,
isAvailable: true,
unavailabilityMessage: null,
};
}
+2 -1
View File
@@ -4,6 +4,7 @@ import { ef } from '@/lib/helpers/utfNumbers'
import { EmojiHappy, Microphone2, Paperclip2 } from 'iconsax-react'
import React, { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { glassSurfaceCard } from '@/lib/styles/glassSurface'
type Props = object
@@ -64,7 +65,7 @@ function ChatIndex ({}: Props) {
<section className='flex flex-col h-full pt-6'>
<h1 className='font-medium'>علی مصلحی</h1>
<div className='bg-container rounded-[30px] flex flex-col justify-between mt-6 flex-1 px-6 pt-7 pb-8 overflow-hidden'>
<div className={glassSurfaceCard('rounded-[30px] flex flex-col justify-between mt-6 flex-1 px-6 pt-7 pb-8 overflow-hidden')}>
<div className='overflow-y-auto flex flex-col-reverse'>
<ul className='grid gap-6 pb-6 '>
{messagesMemo.map((v, i) => {
+4 -3
View File
@@ -16,6 +16,7 @@ import {
import { useRouter } from 'next/navigation'
import useToggle from '@/hooks/helpers/useToggle'
import Prompt from '@/components/utils/Prompt'
import { glassSurfaceFlat } from '@/lib/styles/glassSurface'
type Props = object
@@ -170,7 +171,7 @@ function ChatIndex ({}: Props) {
{selectedChats.length > 0 ? (
<span
onClick={toggleDeleteModal}
className='bg-container p-2 rounded-xl active:bg-gray-50 dark:active:bg-neutral-500'
className={glassSurfaceFlat('p-2 rounded-xl active:brightness-95 cursor-pointer')}
>
<Trash
size={24}
@@ -180,7 +181,7 @@ function ChatIndex ({}: Props) {
) : (
<Link
href={'chat/nearby'}
className='bg-container p-2 rounded-xl active:bg-gray-50 dark:active:bg-neutral-500'
className={glassSurfaceFlat('p-2 rounded-xl active:brightness-95 cursor-pointer')}
>
<Radar2
size={24}
@@ -198,7 +199,7 @@ function ChatIndex ({}: Props) {
<li
key={i}
{...longPress(chat.id)}
className='select-none flex items-center gap-x-2 hover:brightness-105 cursor-default active:brightness-102 p-2 bg-background rounded-xl'
className={glassSurfaceFlat('select-none flex items-center gap-x-2 hover:brightness-105 cursor-default active:brightness-102 p-2 rounded-xl')}
>
<div className='size-[49px] min-w-[49px]'>
{selectedChats.includes(chat.id) ? (
@@ -4,6 +4,7 @@ import { Category } from "@/app/[name]/(Main)/types/Types";
import CategoryItemRenderer from "@/components/listview/CategoryItemRenderer";
import CategorySmallItemRenderer from "@/components/listview/CategorySmallItemRenderer";
import HorizontalScrollView from "@/components/listview/HorizontalScrollView";
import { GLASS_SURFACE_DISABLED, GLASS_SURFACE_SELECTED } from "@/lib/styles/glassSurface";
import clsx from "clsx";
import { usePathname } from "next/navigation";
@@ -19,14 +20,14 @@ const SVG_MASK_STYLE = {
WebkitMaskPosition: "center",
} as const;
function CategoryImage({ src, size, alt, tintWithPrimary = true }: { src: string; size: number; alt: string; tintWithPrimary?: boolean }) {
function CategoryImage({ src, size, alt, tintWithPrimary = true, disabled = false }: { src: string; size: number; alt: string; tintWithPrimary?: boolean; disabled?: boolean }) {
const isSvg = src.endsWith(".svg");
const shouldUseSvgMask = isSvg && tintWithPrimary;
if (shouldUseSvgMask) {
return (
<div
className="shrink-0 bg-primary"
className={clsx("shrink-0 bg-primary", disabled && "opacity-40")}
style={{
width: size,
height: size,
@@ -48,7 +49,7 @@ function CategoryImage({ src, size, alt, tintWithPrimary = true }: { src: string
height={size}
alt={alt}
loading="lazy"
className="shrink-0 object-contain"
className={clsx("shrink-0 object-contain", disabled && "opacity-40")}
onError={(event) => {
event.currentTarget.src = "/assets/images/food-image.png";
}}
@@ -108,11 +109,17 @@ const CategoryScroll = ({ categories, selectedCategory, onSelect, variant = "lar
</Renderer> */}
{categories.map((item) => {
const isSelected = item.id === selectedCategory;
const isDisabled = !item.isActive;
return (
<Renderer key={item.id} className={clsx(isSelected && "bg-container!")} onClick={handleSelect(item.id)}>
<CategoryImage src={item.avatarUrl || "/assets/images/food-image.png"} size={imageSize} alt="category image" tintWithPrimary={!usesColoredSvg} />
<span className="text-xs text-foreground text-center">{item.title}</span>
<Renderer
key={item.id}
className={clsx(!isSelected && GLASS_SURFACE_DISABLED, isDisabled && "cursor-not-allowed", isSelected && !isDisabled && GLASS_SURFACE_SELECTED)}
onClick={isDisabled ? undefined : handleSelect(item.id)}
aria-disabled={isDisabled}
>
<CategoryImage src={item.avatarUrl || "/assets/images/food-image.png"} size={imageSize} alt="category image" tintWithPrimary={!usesColoredSvg} disabled={isDisabled} />
<span className={clsx("text-xs text-center whitespace-nowrap", isDisabled ? "text-disabled-text" : "text-foreground")}>{item.title}</span>
</Renderer>
);
})}
@@ -0,0 +1,194 @@
"use client";
import type { Slider } from "@/app/[name]/(Main)/types/Types";
import clsx from "clsx";
import Link from "next/link";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
type Props = {
sliders: Slider[];
};
const AUTOPLAY_MS = 8000;
function getVisibleSliders(sliders: Slider[]) {
return sliders
.filter((slider) => slider.isActive !== false && slider.imageUrl)
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
}
function SliderSlide({
slider,
priority,
}: {
slider: Slider;
priority?: boolean;
}) {
const image = (
// eslint-disable-next-line @next/next/no-img-element
<img
src={slider.imageUrl}
alt={slider.title || "promo banner"}
loading={priority ? "eager" : "lazy"}
draggable={false}
className="h-full w-full object-cover"
onError={(event) => {
event.currentTarget.src = "/assets/images/food-image.png";
}}
/>
);
const slideContent = (
<div className="relative aspect-[2.4/1] w-full overflow-hidden rounded-2xl">
{image}
{slider.title ? (
<div className="pointer-events-none absolute bottom-6 right-3 max-w-[75%]">
<p className="truncate text-right text-sm font-medium text-white drop-shadow-[0_2px_4px_rgba(0,0,0,0.7)]">
{slider.title}
</p>
</div>
) : null}
</div>
);
if (!slider.link) {
return slideContent;
}
const isExternal = /^https?:\/\//i.test(slider.link);
if (isExternal) {
return (
<a
href={slider.link}
target="_blank"
rel="noopener noreferrer"
className="block w-full shrink-0 snap-center"
aria-label={slider.title || "promo banner"}
>
{slideContent}
</a>
);
}
return (
<Link
href={slider.link}
className="block w-full shrink-0 snap-center"
aria-label={slider.title || "promo banner"}
>
{slideContent}
</Link>
);
}
export default function HomeSliderCarousel({ sliders }: Props) {
const visibleSliders = useMemo(() => getVisibleSliders(sliders), [sliders]);
const scrollRef = useRef<HTMLDivElement>(null);
const slideRefs = useRef<(HTMLDivElement | null)[]>([]);
const autoplayPausedRef = useRef(false);
const [activeIndex, setActiveIndex] = useState(0);
const scrollToIndex = useCallback((index: number) => {
slideRefs.current[index]?.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "start",
});
}, []);
useEffect(() => {
const container = scrollRef.current;
if (!container || visibleSliders.length <= 1) return;
const handleScroll = () => {
const slideWidth = container.clientWidth;
if (!slideWidth) return;
const nextIndex = Math.round(container.scrollLeft / slideWidth);
setActiveIndex(Math.min(nextIndex, visibleSliders.length - 1));
};
container.addEventListener("scroll", handleScroll, { passive: true });
handleScroll();
return () => {
container.removeEventListener("scroll", handleScroll);
};
}, [visibleSliders.length]);
useEffect(() => {
if (visibleSliders.length <= 1) return;
const intervalId = window.setInterval(() => {
if (autoplayPausedRef.current) return;
setActiveIndex((currentIndex) => {
const nextIndex = (currentIndex + 1) % visibleSliders.length;
scrollToIndex(nextIndex);
return nextIndex;
});
}, AUTOPLAY_MS);
return () => {
window.clearInterval(intervalId);
};
}, [scrollToIndex, visibleSliders.length]);
const pauseAutoplay = useCallback(() => {
autoplayPausedRef.current = true;
window.setTimeout(() => {
autoplayPausedRef.current = false;
}, AUTOPLAY_MS * 2);
}, []);
if (visibleSliders.length === 0) {
return null;
}
return (
<section className="mb-1 w-full" aria-label="promotional banners">
<div
ref={scrollRef}
dir="ltr"
className="flex w-full snap-x snap-mandatory overflow-x-auto noscrollbar touch-pan-x"
onTouchStart={pauseAutoplay}
onMouseDown={pauseAutoplay}
>
{visibleSliders.map((slider, index) => (
<div
key={slider.id}
ref={(element) => {
slideRefs.current[index] = element;
}}
className="w-full shrink-0 snap-start"
>
<SliderSlide slider={slider} priority={index === 0} />
</div>
))}
</div>
{visibleSliders.length > 1 ? (
<div className="mt-2 flex items-center justify-center gap-1.5">
{visibleSliders.map((slider, index) => (
<button
key={slider.id}
type="button"
aria-label={`Go to slide ${index + 1}`}
aria-current={index === activeIndex}
onClick={() => {
pauseAutoplay();
setActiveIndex(index);
scrollToIndex(index);
}}
className={clsx(
"h-1.5 rounded-full transition-all duration-200",
index === activeIndex ? "w-5 bg-primary" : "w-1.5 bg-foreground/25",
)}
/>
))}
</div>
) : null}
</section>
);
}
+20 -10
View File
@@ -1,5 +1,6 @@
"use client";
import CategoryScroll from "@/app/[name]/(Main)/components/CategoryScroll";
import HomeSliderCarousel from "@/app/[name]/(Main)/components/HomeSliderCarousel";
import MenuFilterDrawer from "@/app/[name]/(Main)/components/MenuFilterDrawer";
import MenuSkeleton from "@/app/[name]/(Main)/components/MenuSkeleton";
import MenuSortingDrawer from "@/app/[name]/(Main)/components/MenuSortingDrawer";
@@ -9,24 +10,27 @@ import MenuItem from "@/components/listview/MenuItem";
import MenuItemRenderer from "@/components/listview/MenuItemRenderer";
import VerticalScrollView from "@/components/listview/VerticalScrollView";
import useToggle from "@/hooks/helpers/useToggle";
import { glassSurfaceFlat } from "@/lib/styles/glassSurface";
import clsx from "clsx";
import { motion } from "framer-motion";
import { Candle2 } from "iconsax-react";
import { useQueryState } from "next-usequerystate";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useGetCategories, useGetFoods } from "../hooks/useMenuData";
import { useGetCategories, useGetFoods, useGetSliders } from "../hooks/useMenuData";
import type { Category, Food } from "../types/Types";
const sortings = ["PopularityDescending", "RateDescending", "PriceAscending", "PriceDescending"] as const;
const MenuIndex = () => {
const { data: foodsData, isLoading: foodsLoading } = useGetFoods();
const { data: categoriesData, isLoading: categoriesLoading } = useGetCategories();
const { data: foodsData, isPending: foodsPending } = useGetFoods();
const { data: categoriesData, isPending: categoriesPending } = useGetCategories();
const { data: slidersData } = useGetSliders();
const foods = useMemo<Food[]>(() => foodsData?.data || [], [foodsData?.data]);
const categories = useMemo<Category[]>(() => categoriesData?.data || [], [categoriesData?.data]);
const sliders = useMemo(() => slidersData?.data ?? [], [slidersData?.data]);
const isLoading = foodsLoading || categoriesLoading;
const isLoading = foodsPending || categoriesPending;
const { t: tCommon } = useTranslation("common");
const { t: tMenu } = useTranslation("menu", { keyPrefix: "Menu" });
const { state: filterModal, toggle: toggleFilterModal } = useToggle();
@@ -161,11 +165,17 @@ const MenuIndex = () => {
}, [selectedCategory, search, selectedIngredients, selectedDeliveryId, foods, sorting]);
if (isLoading) {
return <MenuSkeleton />;
return (
<div className="flex flex-col gap-4 items-center pt-5 mb-8">
<HomeSliderCarousel sliders={sliders} />
<MenuSkeleton embedded />
</div>
);
}
return (
<div className="flex flex-col gap-4 items-center pt-5 mb-8" ref={wrapperRef}>
<HomeSliderCarousel sliders={sliders} />
<div className="w-full">
<SearchBox value={search} placeholder={tCommon("SearchPlaceholder")} onChange={updateSearch} />
<CategoryScroll categories={categories} selectedCategory={selectedCategory} onSelect={updateCategory} />
@@ -173,13 +183,13 @@ const MenuIndex = () => {
<section className="w-full">
<div className="flex flex-wrap gap-2 items-center relative" ref={smallCategoriesRef}>
<span className="sm:text-base text-sm font-medium max-w-[113px] sm:max-w-auto">{selectedCategory === "0" ? "" : categories.find((c) => c.id === selectedCategory)?.title || ""}</span>
<div className="flex min-w-[247px] flex-1 gap-2 justify-end items-center">
<button onClick={toggleFilterModal} className="rounded-xl h-8 bg-container ps-4 pe-2 py-1.5 inline-flex items-center justify-between gap-[7px]">
<span className="sm:text-base text-sm font-medium max-w-28.25 sm:max-w-auto">{selectedCategory === "0" ? "" : categories.find((c) => c.id === selectedCategory)?.title || ""}</span>
<div className="flex min-w-61.75 flex-1 gap-2 justify-end items-center">
<button onClick={toggleFilterModal} className={glassSurfaceFlat("rounded-xl h-8 ps-4 pe-2 py-1.5 inline-flex items-center justify-between gap-1.75")}>
<Candle2 className="stroke-foreground" size={16} />
<span className="text-xs leading-5 font-medium whitespace-nowrap">{tMenu("MenuFilterDrawer.Label")}</span>
</button>
<button onClick={toggleSortingModal} className="rounded-xl h-8 bg-container ps-4 pe-2 py-1.5 inline-flex items-center gap-2">
<button onClick={toggleSortingModal} className={glassSurfaceFlat("rounded-xl h-8 ps-4 pe-2 py-1.5 inline-flex items-center gap-2")}>
<TextAlignIcon className="text-foreground" />
<span className="text-xs leading-5 font-medium whitespace-nowrap">{activeSortingLabel}</span>
</button>
@@ -196,7 +206,7 @@ const MenuIndex = () => {
opacity: smallCategoriesVisible ? 1 : 0,
}}
transition={{ duration: 0.1 }}
className={clsx("fixed left-0 z-10 top-0 px-4 pt-16 bg-[#F4F5F9CC] dark:bg-background/70 backdrop-blur-[44px] right-0 xl:pr-72 xl:pt-20", !smallCategoriesVisible && "pointer-events-none")}
className={clsx(glassSurfaceFlat("fixed left-0 z-10 top-0 px-4 pt-16 right-0 xl:pr-72 xl:pt-20 border-x-0 border-t-0 rounded-none"), !smallCategoriesVisible && "pointer-events-none")}
>
<CategoryScroll categories={categories} selectedCategory={selectedCategory} onSelect={updateCategory} variant="small" />
</motion.div>
@@ -3,10 +3,15 @@
import MenuItemRenderer from "@/components/listview/MenuItemRenderer";
import VerticalScrollView from "@/components/listview/VerticalScrollView";
import { Skeleton } from "@/components/ui/skeleton";
import clsx from "clsx";
const MenuSkeleton = () => {
type Props = {
embedded?: boolean;
};
const MenuSkeleton = ({ embedded = false }: Props) => {
return (
<div className="flex flex-col gap-4 items-center pt-5 mb-8">
<div className={clsx("flex flex-col gap-4 items-center w-full", !embedded && "pt-5 mb-8")}>
<div className="w-full">
<Skeleton className="h-12 w-full rounded-xl mb-4" />
<div className="flex gap-3 overflow-x-hidden py-4">
+52 -6
View File
@@ -1,28 +1,63 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { useEffect } from "react";
import * as api from "../service/MenuService";
import { useParams } from "next/navigation";
import { hasAuthToken } from "@/lib/api/func";
import {
getPersistedCategoriesData,
getPersistedMenuData,
setPersistedCategoriesData,
setPersistedMenuData,
} from "@/lib/helpers/themeCache";
export const useGetFoods = () => {
const { name } = useParams<{ name: string }>();
return useQuery({
const query = useQuery({
queryKey: ["menu", name],
queryFn: () => api.getFoods(name),
queryFn: async () => {
const data = await api.getFoods(name);
setPersistedMenuData(name, data);
return data;
},
enabled: !!name,
staleTime: 5 * 60_000,
refetchOnMount: false,
refetchOnMount: "always",
placeholderData: () => (name ? getPersistedMenuData(name) : undefined),
});
useEffect(() => {
if (name && query.data) {
setPersistedMenuData(name, query.data);
}
}, [name, query.data]);
return query;
};
export const useGetCategories = () => {
const { name } = useParams<{ name: string }>();
return useQuery({
const query = useQuery({
queryKey: ["categories", name],
queryFn: () => api.getCategories(name),
queryFn: async () => {
const data = await api.getCategories(name);
setPersistedCategoriesData(name, data);
return data;
},
enabled: !!name,
staleTime: 5 * 60_000,
refetchOnMount: false,
refetchOnMount: "always",
placeholderData: () => (name ? getPersistedCategoriesData(name) : undefined),
});
useEffect(() => {
if (name && query.data) {
setPersistedCategoriesData(name, query.data);
}
}, [name, query.data]);
return query;
};
export const useGetNotificationsCount = () => {
@@ -32,3 +67,14 @@ export const useGetNotificationsCount = () => {
enabled: hasAuthToken(),
});
};
export const useGetSliders = () => {
const { name } = useParams<{ name: string }>();
return useQuery({
queryKey: ["sliders", name],
queryFn: () => api.getSliders(name),
enabled: !!name,
staleTime: 5 * 60_000,
refetchOnMount: "always",
});
};
-10
View File
@@ -1,10 +0,0 @@
import LoadingOverlay from '@/components/overlays/LoadingOverlay'
import React from 'react'
function Loading() {
return (
<LoadingOverlay />
)
}
export default Loading
+3 -8
View File
@@ -12,7 +12,7 @@ import React, { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next';
import { useGetOrders } from './hooks/useHistoryData';
import { HistoryOrderItem } from './types/Types';
import { OrderStatus } from './enum/Enum';
import { glassSurface } from '@/lib/styles/glassSurface';
const fallbackImage = '/assets/images/food-preview.png';
@@ -80,7 +80,7 @@ function OrdersIndex() {
const items: HistoryOrderItem[] = order.items || [];
return (
<div key={order.id} className='w-full px-[17px] mb-4 pt-8 pb-6 bg-container rounded-3xl'>
<div key={order.id} className={glassSurface('w-full px-[17px] mb-4 pt-8 pb-6 rounded-3xl')}>
<div className="text-sm2">
<div className='flex items-center justify-between mb-2'>
<div className='flex gap-2 items-center'>
@@ -165,7 +165,7 @@ function OrdersIndex() {
const items: HistoryOrderItem[] = order.items || [];
return (
<div key={order.id} className='w-full px-[17px] mb-4 pt-8 pb-6 bg-container rounded-3xl'>
<div key={order.id} className={glassSurface('w-full px-[17px] mb-4 pt-8 pb-6 rounded-3xl')}>
<div className="text-sm2">
<div className='flex items-center justify-between mb-2'>
<div className='flex gap-2 items-center'>
@@ -220,11 +220,6 @@ function OrdersIndex() {
</span>
</div>
</div>
{order.status === OrderStatus.COMPLETED && (
<Link href={`rate/${order.id}?foodId=${item.food.id}`}>
<Button className='text-xs px-3 py-2 h-auto'>{t('Card.Rate')}</Button>
</Link>
)}
</div>
);
})}
@@ -79,12 +79,10 @@ export interface HistoryOrderDeliveryMethod {
}
export interface HistoryOrderUserAddress {
city: string;
phone: string;
address: string;
fullName: string;
latitude: number;
province: string;
longitude: number;
postalCode: string;
}
@@ -97,8 +95,6 @@ export interface HistoryOrderAddress {
user: string;
title: string;
address: string;
city: string;
province: string;
postalCode: string;
latitude: number;
longitude: number;
@@ -1,223 +0,0 @@
'use client';
import ToggleButton, { ToggleButtonClassNames } from '@/components/button/ToggleButton';
import TabContainer, { TabContainerClassNames, TabContainerRenderType } from '@/components/tab/TabContainer';
import { TabHeader } from '@/components/tab/TabHeader';
import { Button } from '@/components/ui/button';
import RateSelectionBar from '@/components/utils/RateSelectionBar';
import clsx from 'clsx';
import { useParams, useRouter } from 'next/navigation';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useCreateRate } from '../hooks/useRateData';
import { CreateRateType } from '../types/Types';
import { toast } from '@/components/Toast';
import { PositivePointEnum, NegativePointEnum } from '../enum/Enum';
import { extractErrorMessage } from '@/lib/func';
type Props = object
type Params = {
orderId: string;
}
function RatingOrderIndex({ }: Props) {
const { t } = useTranslation("rating");
const params: Params = useParams();
const router = useRouter();
const urlParams = new URLSearchParams(window.location.search);
const { mutate: createRate, isPending } = useCreateRate();
const [rating, setRating] = useState<number>(5);
const [comment, setComment] = useState<string>('');
const [positivePoints, setPositivePoints] = useState<string[]>([]);
const [negativePoints, setNegativePoints] = useState<string[]>([]);
const positivePointsArray = [
PositivePointEnum.GREAT_TASTE,
PositivePointEnum.FAST_DELIVERY,
PositivePointEnum.GOOD_QUALITY,
PositivePointEnum.GOOD_PORTION_SIZE,
PositivePointEnum.FRIENDLY_SERVICE,
];
const negativePointsArray = [
NegativePointEnum.SMALL_PORTION,
NegativePointEnum.SLOW_DELIVERY,
NegativePointEnum.POOR_QUALITY,
NegativePointEnum.OVERPRICED,
NegativePointEnum.UNFRIENDLY_SERVICE,
];
const getTranslationKey = (point: string, isPositive: boolean): string => {
const basePath = isPositive ? 'Tabs.Strengths.Options' : 'Tabs.Weeknesses.Options';
return `${basePath}.${point}`;
};
const handleToggle = (pointValue: string, isPositive: boolean) => (checked: boolean) => {
const setter = isPositive ? setPositivePoints : setNegativePoints;
if (checked) {
setter(prev => [...prev, pointValue]);
} else {
setter(prev => prev.filter(p => p !== pointValue));
}
};
const badPoitns = () => {
return (
<>
<div className="grid grid-cols-2 gap-x-4.5 gap-y-4 mt-8">
{negativePointsArray.map((point) => (
<ToggleButton
className={{
...ToggleButtonClassNames,
wrapperActive: 'border-red-400',
contentActive: 'text-red-400'
}}
key={point}
name={`weekness_${point}`}
// @ts-expect-error - Type mismatch between custom ToggleButton and React types
onToggle={handleToggle(point, false)}
>
{t(getTranslationKey(point, false))}
</ToggleButton>
))}
</div>
</>
)
}
const goodPoitns = () => {
return (
<>
<div className="grid grid-cols-2 gap-x-4.5 gap-y-4 mt-8">
{positivePointsArray.map((point) => (
<ToggleButton
key={point}
name={`strength_${point}`}
// @ts-expect-error - Type mismatch between custom ToggleButton and React types
onToggle={handleToggle(point, true)}
>
{t(getTranslationKey(point, true))}
</ToggleButton>
))}
</div>
</>
)
}
const handleRatingChange = (value: number[]) => {
setRating(value[0]);
};
const handleCancel = () => {
router.back();
};
const submitForm = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (rating === 0) {
toast(t("Errors.RatingRequired") || "لطفا امتیاز خود را انتخاب کنید", "error");
return;
}
const rateData: CreateRateType = {
foodId: urlParams.get('foodId') || '',
orderId: params.orderId,
rating,
comment,
positivePoints,
negativePoints,
};
createRate(rateData, {
onSuccess: () => {
toast(t("Success") || "نظر شما با موفقیت ثبت شد", "success");
router.back();
},
onError: (error) => {
toast(extractErrorMessage(error), "error");
},
});
};
return (
<section className='flex flex-col h-full pt-6'>
<h1 className='font-medium'>{t("Heading")}</h1>
<form onSubmit={submitForm} className='bg-container flex flex-col h-min justify-between rounded-[30px] pt-10 px-6 pb-7.5 mt-4'>
<div>
<div className='w-full'>
<RateSelectionBar
name='rating'
defaultValue={[rating]}
onValueChange={handleRatingChange}
/>
</div>
<div className='mt-10'>
<TabContainer
defaultIndex={1}
changeType={TabContainerRenderType.VISIBILITY}
className={{
...TabContainerClassNames,
wrapper: 'p-2!',
scrollView: clsx(
'border-none! rounded-xl! gap-0! h-full bg-[#EAECF0]! dark:bg-neutral-900! not-dark:gradient-border! grid! grid-cols-2 px-2! py-2! pb-2! w-full overflow-y-hidden justify-center items-center',
),
title: 'text-sm2! font-normal mt-1',
header: 'rounded-lg h-7! w-full',
headerActive: 'bg-white',
titleActive: 'text-primary'
}}>
<TabHeader title={t("Tabs.Weeknesses.Title")} viewRenderer={badPoitns()}></TabHeader>
<TabHeader title={t("Tabs.Strengths.Title")} viewRenderer={goodPoitns()}></TabHeader>
</TabContainer>
<div className="w-full mt-6">
<label htmlFor='userOpinion' className='text-sm font-medium'>
{t("InputComment.Label")}
</label>
<br />
<textarea
name='userOpinion'
id='userOpinion'
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder={t("InputComment.Placeholder")}
className='dark:border-neutral-600 dark:bg-neutral-800 mt-2 w-full h-21 px-4 py-2.5 text-xs text-foreground rounded-xl border border-[#D0D0D0] focus:outline-none focus:ring-2 focus:ring-accent'
/>
<br />
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mt-6">
<Button
type='submit'
disabled={isPending}
className='mt-auto rounded-xl font-normal cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed'
>
{isPending ? t("ButtonSubmitting") || "در حال ارسال..." : t("ButtonSubmit")}
</Button>
<Button
type='button'
onClick={handleCancel}
disabled={isPending}
className='
mt-auto rounded-xl bg-white dark:bg-container cursor-pointer hover:bg-neutral-100 dark:hover:bg-neutral-700
border border-foreground dark:border-neutral-600 text-foreground font-normal disabled:opacity-50 disabled:cursor-not-allowed'
>
{t("ButtonCancel")}
</Button>
</div>
</form>
</section>
)
}
export default RatingOrderIndex
@@ -1,8 +0,0 @@
import { useMutation } from "@tanstack/react-query";
import * as api from "../service/RateService";
export const useCreateRate = () => {
return useMutation({
mutationFn: api.createRate,
});
};
@@ -1,7 +0,0 @@
import { api } from "@/config/axios";
import { CreateRateType } from "../types/Types";
export const createRate = async (params: CreateRateType) => {
const { data } = await api.post("/public/reviews", params);
return data;
};
@@ -1,8 +0,0 @@
export type CreateRateType = {
foodId: string;
orderId: string;
rating: number;
comment: string;
positivePoints: string[];
negativePoints: string[];
};
@@ -3,6 +3,7 @@ import {
CategoriesResponse,
FoodsResponse,
NotificationsCountResponse,
SlidersResponse,
} from "../types/Types";
export const getFoods = async (slug: string): Promise<FoodsResponse> => {
@@ -29,3 +30,14 @@ export const getNotificationsCount = async (): Promise<
);
return response.data;
};
export const getSliders = async (slug: string): Promise<SlidersResponse> => {
try {
const response = await api.get<SlidersResponse>(
`/public/sliders/restaurant/${slug}`,
);
return response.data;
} catch {
return { success: true, data: [] };
}
};
@@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button';
import { useGetAbout } from '../../../about/hooks/useAboutData';
import { toast } from '@/components/Toast';
import { extractErrorMessage } from '@/lib/func';
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
function TransactionsIndex() {
@@ -37,7 +38,7 @@ function TransactionsIndex() {
<section
aria-label="درباره باشگاه مشتریان"
className='mt-3 bg-container dark:bg-container rounded-normal grid grid-cols-4 items-center border border-transparent dark:border-border/50'
className={glassSurfaceCard('mt-3 rounded-normal grid grid-cols-4 items-center')}
>
<div className='col-span-3 py-5 pe-7 ps-5 relative h-36.5'>
<div className='w-full h-full absolute top-0 left-0 overflow-clip text-muted-foreground/30 dark:text-foreground/20'>
@@ -78,7 +79,7 @@ function TransactionsIndex() {
</section>
{/* بخش نمایش اعتبار کاربر */}
<section className='mt-6 bg-container dark:bg-container rounded-normal p-5 border border-transparent dark:border-border/50'>
<section className={glassSurfaceCard('mt-6 rounded-normal p-5')}>
<div className='flex items-center gap-2 mb-4'>
<MoneyRecive color='currentColor' size={24} className='text-primary dark:text-foreground' variant='Bold' />
<h2 className='font-bold text-base dark:text-foreground'>اعتبار شما</h2>
@@ -92,7 +93,7 @@ function TransactionsIndex() {
</section>
{/* بخش تبدیل امتیاز به اعتبار */}
<section className='mt-6 bg-container dark:bg-container rounded-normal p-5 border border-transparent dark:border-border/50'>
<section className={glassSurfaceCard('mt-6 rounded-normal p-5')}>
<div className='flex items-center gap-2 mb-4'>
<MoneyRecive color='currentColor' size={24} className='text-primary dark:text-foreground' variant='Bold' />
<h2 className='font-bold text-base dark:text-foreground'>تبدیل امتیاز به اعتبار</h2>
@@ -4,6 +4,7 @@ import React, { useCallback } from 'react'
import { useGetMyCoupons } from '../hooks/useTransactionData';
import { toast } from '@/components/Toast';
import type { Coupon } from '../types/Types';
import { glassSurfaceCard, glassSurfaceCardFlat } from '@/lib/styles/glassSurface';
function TransactionsIndex() {
const { data: myCoupons, isLoading } = useGetMyCoupons();
@@ -32,7 +33,7 @@ function TransactionsIndex() {
<section
aria-label="درباره کدهای تخفیف"
className='mt-3 bg-container rounded-normal grid grid-cols-4 items-center'
className={glassSurfaceCard('mt-3 rounded-normal grid grid-cols-4 items-center')}
>
<div className='col-span-3 py-9.5 pe-7 ps-5 relative'>
<div className='w-full h-full absolute top-0 left-0 overflow-clip'>
@@ -94,7 +95,7 @@ function TransactionsIndex() {
{coupons.map((coupon) => (
<article
key={coupon.id}
className='bg-container rounded-normal flex justify-between items-center'
className={glassSurfaceCardFlat('rounded-normal flex justify-between items-center')}
>
<div className='py-4 pe-7 ps-5 border-e-2 w-full border-border border-dashed'>
<h3 className='font-bold text-sm2'>مبلغ تخفیف: {formatDiscount(coupon)}</h3>
+2 -1
View File
@@ -3,6 +3,7 @@ import React from 'react'
import { useGetTransactions } from './hooks/useTransactionData';
import { TransactionsTable } from './components/TransactionsTable';
import LoadingOverlay from '@/components/overlays/LoadingOverlay';
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
function TransactionsReviewIndex() {
const { data: transactionsResponse, isLoading: isLoadingTransactions } = useGetTransactions();
@@ -56,7 +57,7 @@ function TransactionsReviewIndex() {
</div>
</div> */}
<div className='w-full h-full bg-container rounded-3xl mt-0'>
<div className={glassSurfaceCard('w-full h-full mt-0 rounded-3xl')}>
<TransactionsTable transactions={transactions} />
</div>
</section>
+19 -1
View File
@@ -45,6 +45,8 @@ export interface Food {
pickupServe: boolean;
discount: number;
isSpecialOffer: boolean;
isAvailable: boolean;
unavailabilityMessage: string | null;
}
/** جزئیات کامل یک غذا (endpoint تکی) */
@@ -71,10 +73,12 @@ export interface FoodDetail {
score: number;
discount: number;
isSpecialOffer: boolean;
isAvailable: boolean;
unavailabilityMessage: string | null;
reviews: unknown[];
favorites: unknown[];
isFavorite: boolean;
// فیلدهای قدیمی برای سازگاری با سایر بخش‌های کد
name?: string;
foodName?: string;
description?: string;
@@ -100,3 +104,17 @@ export interface NotificationsCountData {
}
export type NotificationsCountResponse = BaseResponse<NotificationsCountData>;
export interface Slider {
id: string;
createdAt?: string;
updatedAt?: string;
deletedAt?: string | null;
imageUrl: string;
title?: string | null;
link?: string | null;
order?: number;
isActive?: boolean;
}
export type SlidersResponse = BaseResponse<Slider[]>;
@@ -36,6 +36,8 @@ function FavoritePage() {
pickupServe: favoriteFood.pickupServe,
discount: favoriteFood.discount,
isSpecialOffer: favoriteFood.isSpecialOffer,
isAvailable: true,
unavailabilityMessage: null,
}
return food
})
@@ -58,7 +58,7 @@ export const AddressDetailsModal = ({
e.preventDefault();
onChangePosition();
}}
className='bg-background! w-fit! text-foreground! flex items-center gap-2 text-xs'
className='bg-background! pattern-secondary-bg w-fit! text-foreground! flex items-center gap-2 text-xs'
disabled={isPending}
>
<Location color='#6b7280' variant='Bold' className='size-4' />
@@ -1,90 +1,80 @@
import InputField from '@/components/input/InputField';
import { NominatimReverseGeocodingResponse } from '../../types/Types';
import { useGetProfile } from '../../../hooks/userProfileData';
import { useEffect } from 'react';
import InputField from "@/components/input/InputField";
import { useEffect } from "react";
import { useGetProfile } from "../../../hooks/userProfileData";
import { NominatimReverseGeocodingResponse } from "../../types/Types";
interface AddressFormProps {
formData: {
title: string;
addressDetails: string;
phone: string;
postalCode: string;
isDefault: boolean;
};
selectedAddress: NominatimReverseGeocodingResponse | null;
onFormDataChange: (data: Partial<AddressFormProps['formData']>) => void;
formData: {
title: string;
addressDetails: string;
phone: string;
postalCode: string;
isDefault: boolean;
};
selectedAddress: NominatimReverseGeocodingResponse | null;
onFormDataChange: (data: Partial<AddressFormProps["formData"]>) => void;
}
export const AddressForm = ({
formData,
selectedAddress,
onFormDataChange,
}: AddressFormProps) => {
export const AddressForm = ({ formData, selectedAddress, onFormDataChange }: AddressFormProps) => {
const { data: profileData } = useGetProfile();
const { data: profileData } = useGetProfile()
useEffect(() => {
if (profileData?.data?.phone && !formData.phone) {
onFormDataChange({ phone: "0" + profileData.data.phone });
}
}, [profileData?.data?.phone, formData.phone, onFormDataChange]);
useEffect(() => {
if (profileData?.data?.phone && !formData.phone) {
onFormDataChange({ phone: '0' + profileData.data.phone });
}
}, [profileData?.data?.phone, formData.phone, onFormDataChange]);
return (
<div className='px-4'>
<InputField
htmlFor={'addressTitle'}
labelText={'عنوان آدرس'}
placeholder='مثال: منزل، محل کار ...'
className='bg-inherit'
inputClassName='text-xs!'
value={formData.title}
onChange={(e) => onFormDataChange({ title: e.target.value })}
/>
<InputField
htmlFor={'addressDetails'}
labelText={'جزئیات آدرس'}
placeholder='میدان باغ ملی، مجتمع آسمان، واحد 12، پلاک 1234567890'
className='bg-inherit mt-8'
value={formData.addressDetails}
onChange={(e) => onFormDataChange({ addressDetails: e.target.value })}
inputClassName='text-xs!'
/>
<InputField
htmlFor={'phone'}
labelText={'شماره تلفن'}
placeholder='09123456789'
className='bg-inherit mt-8'
value={formData.phone}
onChange={(e) => onFormDataChange({ phone: e.target.value })}
inputClassName='text-xs!'
/>
<InputField
htmlFor={'postalCode'}
labelText={'کد پستی'}
placeholder={selectedAddress?.address?.postcode || '1234567890'}
className='bg-inherit mt-8'
value={formData.postalCode || selectedAddress?.address?.postcode || ''}
onChange={(e) => onFormDataChange({ postalCode: e.target.value })}
inputClassName='text-xs!'
/>
<div className='inline-flex justify-between items-center w-full mt-8'>
<label
htmlFor={'isDefault'}
className='text-sm2 px-2 py-0.5 mt-0.5 cursor-pointer'
>
تنظیم به عنوان آدرس پیشفرض
</label>
<input
name={'isDefault'}
id={'isDefault'}
className='h-4.5 w-4.5 checked:accent-primary cursor-pointer'
onChange={(e) => onFormDataChange({ isDefault: e.target.checked })}
type='checkbox'
checked={formData.isDefault}
/>
</div>
</div>
);
return (
<div className="px-4">
<InputField
htmlFor={"addressTitle"}
labelText={"عنوان آدرس"}
placeholder="مثال: منزل، محل کار ..."
className="bg-inherit"
inputClassName="text-xs!"
value={formData.title}
onChange={(e) => onFormDataChange({ title: e.target.value })}
/>
<InputField
htmlFor={"addressDetails"}
labelText={"جزئیات آدرس"}
placeholder="میدان باغ ملی، مجتمع آسمان، واحد 12، پلاک 1234567890"
className="bg-inherit mt-8"
value={formData.addressDetails}
onChange={(e) => onFormDataChange({ addressDetails: e.target.value })}
inputClassName="text-xs!"
/>
<InputField
htmlFor={"phone"}
labelText={"شماره تلفن"}
placeholder="09123456789"
className="bg-inherit mt-8"
value={formData.phone}
onChange={(e) => onFormDataChange({ phone: e.target.value })}
inputClassName="text-xs!"
/>
<InputField
htmlFor={"postalCode"}
labelText={"کد پستی"}
placeholder={selectedAddress?.address?.postcode || "1234567890"}
className="bg-inherit mt-8"
value={formData.postalCode || selectedAddress?.address?.postcode || ""}
onChange={(e) => onFormDataChange({ postalCode: e.target.value })}
inputClassName="text-xs!"
/>
<div className="inline-flex justify-between items-center w-full mt-8">
<label htmlFor={"isDefault"} className="text-sm2 px-2 py-0.5 mt-0.5 cursor-pointer">
تنظیم به عنوان آدرس پیشفرض
</label>
<input
name={"isDefault"}
id={"isDefault"}
className="h-4.5 w-4.5 checked:accent-primary cursor-pointer"
onChange={(e) => onFormDataChange({ isDefault: e.target.checked })}
type="checkbox"
checked={formData.isDefault}
/>
</div>
</div>
);
};
@@ -1,134 +1,124 @@
import { useState, useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useCreateAddress, useUpdateAddress } from '../../hooks/useAddressData';
import { toast } from '@/components/Toast';
import { CreateAddressType, UpdateAddressType, NominatimReverseGeocodingResponse, Address } from '../../types/Types';
import { toast } from "@/components/Toast";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import { useCreateAddress, useUpdateAddress } from "../../hooks/useAddressData";
import { Address, CreateAddressType, NominatimReverseGeocodingResponse, UpdateAddressType } from "../../types/Types";
interface UseAddressFormProps {
selectedPosition: [number, number] | null;
selectedAddress: NominatimReverseGeocodingResponse | null;
fallbackPosition?: [number, number] | null;
initialPhone?: string;
editMode?: boolean;
addressData?: Address | null;
selectedPosition: [number, number] | null;
selectedAddress: NominatimReverseGeocodingResponse | null;
fallbackPosition?: [number, number] | null;
initialPhone?: string;
editMode?: boolean;
addressData?: Address | null;
}
export const useAddressForm = ({
selectedPosition,
selectedAddress,
fallbackPosition = [0, 0],
initialPhone = '',
editMode = false,
addressData = null,
}: UseAddressFormProps) => {
const router = useRouter();
const searchParams = useSearchParams();
const redirect = searchParams.get('redirect');
const { mutate: createAddress, isPending: isCreating } = useCreateAddress();
const { mutate: updateAddress, isPending: isUpdating } = useUpdateAddress();
const isPending = isCreating || isUpdating;
const [formData, setFormData] = useState({
title: '',
addressDetails: '',
phone: initialPhone,
postalCode: '',
isDefault: false,
});
export const useAddressForm = ({ selectedPosition, selectedAddress, fallbackPosition = [0, 0], initialPhone = "", editMode = false, addressData = null }: UseAddressFormProps) => {
const router = useRouter();
const searchParams = useSearchParams();
const redirect = searchParams.get("redirect");
const { mutate: createAddress, isPending: isCreating } = useCreateAddress();
const { mutate: updateAddress, isPending: isUpdating } = useUpdateAddress();
const isPending = isCreating || isUpdating;
useEffect(() => {
if (editMode && addressData) {
setFormData({
title: addressData.title || '',
addressDetails: addressData.address || '',
phone: addressData.phone || initialPhone,
postalCode: addressData.postalCode || '',
isDefault: addressData.isDefault || false,
});
}
}, [editMode, addressData, initialPhone]);
const [formData, setFormData] = useState({
title: "",
addressDetails: "",
phone: initialPhone,
postalCode: "",
isDefault: false,
});
const handleFormDataChange = (data: Partial<typeof formData>) => {
setFormData((prev) => ({ ...prev, ...data }));
useEffect(() => {
if (editMode && addressData) {
setFormData({
title: addressData.title || "",
addressDetails: addressData.address || "",
phone: addressData.phone || initialPhone,
postalCode: addressData.postalCode || "",
isDefault: addressData.isDefault || false,
});
}
}, [editMode, addressData, initialPhone]);
const handleFormDataChange = (data: Partial<typeof formData>) => {
setFormData((prev) => ({ ...prev, ...data }));
};
const submitAddress = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const positionToUse: [number, number] = selectedPosition || fallbackPosition || [0, 0];
if (!formData.title.trim()) {
toast("لطفا عنوان آدرس را وارد کنید", "error");
return;
}
if (!formData.addressDetails.trim()) {
toast("لطفا جزئیات آدرس را وارد کنید", "error");
return;
}
if (!formData.phone.trim()) {
toast("لطفا شماره تلفن را وارد کنید", "error");
return;
}
const baseAddressData = {
title: formData.title,
address: formData.addressDetails,
postalCode: formData.postalCode || selectedAddress?.address?.postcode || addressData?.postalCode || "",
latitude: positionToUse[0],
longitude: positionToUse[1],
phone: formData.phone,
isDefault: formData.isDefault,
};
const submitAddress = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (editMode && addressData) {
const updateData: UpdateAddressType = {
...baseAddressData,
id: addressData.id,
};
const positionToUse: [number, number] = selectedPosition || fallbackPosition || [0, 0];
updateAddress(updateData, {
onSuccess: () => {
toast("آدرس با موفقیت به‌روزرسانی شد", "success");
if (redirect) {
router.replace(redirect);
} else {
router.back();
}
},
onError: (error) => {
const errorMessage = error instanceof Error ? error.message : "خطا در به‌روزرسانی آدرس";
toast(errorMessage, "error");
},
});
} else {
const createData: CreateAddressType = baseAddressData;
if (!formData.title.trim()) {
toast('لطفا عنوان آدرس را وارد کنید', 'error');
return;
}
createAddress(createData, {
onSuccess: () => {
toast("آدرس با موفقیت ثبت شد", "success");
if (redirect) {
router.replace(redirect);
} else {
router.back();
}
},
onError: (error) => {
const errorMessage = error instanceof Error ? error.message : "خطا در ثبت آدرس";
toast(errorMessage, "error");
},
});
}
};
if (!formData.addressDetails.trim()) {
toast('لطفا جزئیات آدرس را وارد کنید', 'error');
return;
}
if (!formData.phone.trim()) {
toast('لطفا شماره تلفن را وارد کنید', 'error');
return;
}
const baseAddressData = {
title: formData.title,
address: formData.addressDetails,
city: selectedAddress?.address?.city || selectedAddress?.address?.district || addressData?.city || '',
province: selectedAddress?.address?.province || selectedAddress?.address?.state || addressData?.province || '',
postalCode: formData.postalCode || selectedAddress?.address?.postcode || addressData?.postalCode || '',
latitude: positionToUse[0],
longitude: positionToUse[1],
phone: formData.phone,
isDefault: formData.isDefault,
};
if (editMode && addressData) {
const updateData: UpdateAddressType = {
...baseAddressData,
id: addressData.id,
};
updateAddress(updateData, {
onSuccess: () => {
toast('آدرس با موفقیت به‌روزرسانی شد', 'success');
if (redirect) {
router.replace(redirect);
} else {
router.back();
}
},
onError: (error) => {
const errorMessage = error instanceof Error ? error.message : 'خطا در به‌روزرسانی آدرس';
toast(errorMessage, 'error');
},
});
} else {
const createData: CreateAddressType = baseAddressData;
createAddress(createData, {
onSuccess: () => {
toast('آدرس با موفقیت ثبت شد', 'success');
if (redirect) {
router.replace(redirect);
} else {
router.back();
}
},
onError: (error) => {
const errorMessage = error instanceof Error ? error.message : 'خطا در ثبت آدرس';
toast(errorMessage, 'error');
},
});
}
};
return {
formData,
isPending,
handleFormDataChange,
submitAddress,
};
return {
formData,
isPending,
handleFormDataChange,
submitAddress,
};
};
@@ -1,7 +1,7 @@
import { Address, NominatimReverseGeocodingResponse } from '../../types/Types';
export const formatAddress = (address: Address): string => {
return `${address.address}، ${address.city}، ${address.province}`;
return address.address;
};
export const formatSelectedAddress = (selectedAddress: NominatimReverseGeocodingResponse | null): string => {
@@ -11,8 +11,6 @@ export const formatSelectedAddress = (selectedAddress: NominatimReverseGeocoding
try {
return [
selectedAddress.address.state ?? selectedAddress.address.province,
selectedAddress.address.city,
selectedAddress.address.neighbourhood,
!selectedAddress.address.road.startsWith('میدان') &&
!selectedAddress.address.road.startsWith('بلوار') &&
@@ -11,6 +11,7 @@ import { Address } from './types/Types';
import Prompt from '@/components/utils/Prompt';
import { toast } from '@/components/Toast';
import useToggle from '@/hooks/helpers/useToggle';
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
type Props = object
@@ -69,7 +70,7 @@ function UserAddressesPage({ }: Props) {
};
const formatAddress = (address: Address) => {
return `${address.address}، ${address.city}، ${address.province}`;
return address.address;
};
const getEditHref = (addressId: string) => {
@@ -107,7 +108,7 @@ function UserAddressesPage({ }: Props) {
addresses.map((address) => (
<div
key={address.id}
className={`bg-container rounded-container w-full shadow-xl px-4 py-4 flex flex-col justify-between ${address.isDefault ? 'border border-primary' : ''}`}
className={glassSurfaceCard(`w-full px-4 py-4 flex flex-col justify-between ${address.isDefault ? 'border border-primary' : ''}`)}
>
<div className="flex justify-between items-start mb-2">
<h2 className='text-sm font-medium'>{address.title}</h2>
@@ -132,7 +133,7 @@ function UserAddressesPage({ }: Props) {
onClick={() => handleSetDefault(address.id)}
disabled={isSettingDefault}
pending={isSettingDefault}
className='bg-background! text-foreground! flex items-center gap-2 text-xs h-8 flex-1'
className='bg-background! pattern-secondary-bg text-foreground! flex items-center gap-2 text-xs h-8 flex-1'
>
<TickCircle variant='Bold' className='fill-foreground stroke-background size-4.5 mb-0.5' />
تنظیم به عنوان فعال
@@ -140,14 +141,14 @@ function UserAddressesPage({ }: Props) {
)}
<div className="flex gap-2">
<Link href={getEditHref(address.id)}>
<Button className='bg-background! text-foreground! flex justify-center items-center gap-2 text-xs p-0! size-8!'>
<Button className='bg-background! pattern-secondary-bg text-foreground! flex justify-center items-center gap-2 text-xs p-0! size-8!'>
<Edit2 className='stroke-foreground size-5 mb-0.5' />
</Button>
</Link>
<Button
onClick={() => handleDeleteClick(address.id)}
disabled={isDeleting}
className='bg-background! text-foreground! flex justify-center items-center gap-2 text-xs p-0! size-8!'
className='bg-background! pattern-secondary-bg text-foreground! flex justify-center items-center gap-2 text-xs p-0! size-8!'
>
<Trash className='stroke-foreground size-5 mb-0.5' />
</Button>
@@ -9,8 +9,6 @@ export interface Address {
user: string;
title: string;
address: string;
city: string;
province: string;
postalCode: string;
latitude: number;
longitude: number;
@@ -25,8 +23,6 @@ export type AddressResponse = BaseResponse<Address>;
export type CreateAddressType = {
title: string;
address: string;
city: string;
province: string;
postalCode: string;
latitude: number;
longitude: number;
@@ -13,6 +13,7 @@ import Calendar from '@/components/ui/calendar';
import { getDateLib } from "react-day-picker/persian";
import { useRouter } from 'next/navigation';
import { useGetProfile, useSingleUpload, useUpdateProfile } from '../hooks/userProfileData';
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import { UpdateProfileType } from '../types/Types';
@@ -197,7 +198,7 @@ function ProfileIndex({ }: Props) {
<h1 className='text-sm2 place-self-center font-medium'>ویرایش اطلاعات</h1>
</div>
<form onSubmit={formik.handleSubmit} className="mt-8 flex-1 bg-container rounded-container w-full box-shadow-normal py-6 px-4 flex flex-col justify-between">
<form onSubmit={formik.handleSubmit} className={glassSurfaceCard('mt-8 flex-1 w-full py-6 px-4 flex flex-col justify-between')}>
<div className="bg-inherit">
<div className="justify-self-center w-fit relative text-center pb-10">
<input
+2 -1
View File
@@ -9,6 +9,7 @@ import Link from 'next/link';
import { useRouter } from 'next/navigation';
import React from 'react'
import { useGetProfile } from './hooks/userProfileData';
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
type Props = object
@@ -43,7 +44,7 @@ function ProfileIndex({ }: Props) {
/>
</div>
<div className="mt-8 flex-1 h-full bg-container rounded-container w-full box-shadow-normal py-6 px-4 flex flex-col justify-between">
<div className={glassSurfaceCard('mt-8 flex-1 h-full w-full py-6 px-4 flex flex-col justify-between')}>
<div className="">
<div className="flex items-center justify-start gap-3 pb-6 border-b-[1.5px] border-border">
+185 -246
View File
@@ -1,259 +1,198 @@
'use client';
"use client";
import Button from '@/components/button/PrimaryButton';
import PasswordField from '@/components/input/PasswordField';
import TabContainer from '@/components/tab/TabContainer'
import { TabHeader } from '@/components/tab/TabHeader'
import { Switch } from '@/components/ui/switch';
import Accordion from '@/components/utils/Accordion';
import Seperator from '@/components/utils/Seperator';
import { SmartIcon } from '@/components/utils/SmartIcon';
import { getI18nObject } from '@/lib/i18n';
import { SettingsData } from '@/types/i18n/settings';
import { Key, KeySquare, Notification1, ArrowLeft, TickCircle } from 'iconsax-react'
import { useRouter } from 'next/navigation';
import React, { Fragment, useState } from 'react'
import { useTranslation } from 'react-i18next';
import Button from "@/components/button/PrimaryButton";
import PasswordField from "@/components/input/PasswordField";
import { Switch } from "@/components/ui/switch";
import Accordion from "@/components/utils/Accordion";
import Seperator from "@/components/utils/Seperator";
import { SmartIcon } from "@/components/utils/SmartIcon";
import { getI18nObject } from "@/lib/i18n";
import { glassSurfaceCard } from "@/lib/styles/glassSurface";
import { SettingsData } from "@/types/i18n/settings";
import { ArrowLeft, Key, TickCircle } from "iconsax-react";
import { useRouter } from "next/navigation";
import React, { Fragment, useState } from "react";
import { useTranslation } from "react-i18next";
function UserSettingsIndex() {
const router = useRouter();
const { t } = useTranslation('settings');
const data: SettingsData = getI18nObject(t, "data");
const [statisticalParticipation, setStatisticalParticipation] = useState(true);
const router = useRouter();
const { t } = useTranslation("settings");
const data: SettingsData = getI18nObject(t, "data");
const [statisticalParticipation, setStatisticalParticipation] = useState(true);
const onSwitched = (e: React.MouseEvent) => {
const target = e.target as HTMLElement;
console.log('Save changed setting?', target.id);
}
const onSwitched = (e: React.MouseEvent) => {
const target = e.target as HTMLElement;
console.log("Save changed setting?", target.id);
};
const handleStatisticalToggle = (checked: boolean) => {
setStatisticalParticipation(checked);
console.log('Statistical participation:', checked);
}
const notifsTab = () => {
return (
<section aria-labelledby="about-title" className='py-4 flex-1 h-full'>
<section
className="bg-container rounded-container shadow-container p-4">
<div className="flex justify-between items-center pb-[25px]">
<h2 className='text-sm leading-5'>{data.TabNotifications.Heading}</h2>
</div>
<div className='p-2 pt-0'>
{data.TabNotifications.Accordions.map((accordion, i) => {
return (
<Fragment key={i}>
<Accordion
title={accordion.Heading}
group={'accordions'}
icon={accordion.Icon}
>
<div className='grid gap-4 mb-3'>
{accordion.Items.map((item, j) => {
return (
<label
key={j}
htmlFor={item.HtmlName}
className="flex w-full h-11 items-center justify-between gap-2 px-3 py-4 relative bg-container/29 rounded-xl border border-solid border-neutral-200 dark:border-neutral-600 focus:outline-none focus:ring-2 focus:ring-black"
>
<span className="inline-flex items-center gap-2.5 text-sm2">
<SmartIcon icon={item.Icon} />
{item.Label}
</span>
<Switch
onClick={onSwitched}
id={item.HtmlName}
name={item.HtmlName}
className="w-12 h-6 scale-[0.9]"
/>
</label>
)
})}
</div>
</Accordion>
<Seperator className='last:hidden' />
</Fragment>
)
})}
</div>
</section>
</section>
)
}
const passwordTab = () => {
return (
<section aria-labelledby="reviews-title" className='py-4'>
<section
className="bg-container rounded-container shadow-container p-4">
<div className="flex justify-between items-center pb-[25px]">
<h2 className='text-sm leading-5'>{data.TabPassword.Heading}</h2>
</div>
<form
onSubmit={(e) => e.preventDefault()}
className='grid gap-6 p-2'
>
{data.TabPassword.Inputs.map((input, i) => {
return (
<PasswordField
labelText={input.Label}
className='bg-container'
key={i}
htmlFor={input.HtmlName ?? ''}
placeholder={input.Placeholder}
autoComplete={input.AutoComplete}
/>
)
})
}
{data.TabPassword.Rules.map((rule, i) => {
return (
<section
key={i}
className='bg-background rounded-normal p-4 text-xs flex flex-col gap-2 font-light'
>
<h6 className='text-sm mb-2'>{rule.Heading}</h6>
{rule.Items.map((item, i) => {
return (
<li key={i}>{item}</li>
)
})}
</section>
)
})}
<Button className='mt-4'>
<div className='flex items-center justify-center gap-1'>
<TickCircle className='stroke-white' size={20} />
<span className='mt-0.5'>
{data.TabPassword.ButtonSubmit}
</span>
</div>
</Button>
</form>
</section>
</section>
)
}
const authenticatorTab = () => {
return (
<section aria-labelledby="reviews-title" className='py-4'>
<div
className="bg-container rounded-container shadow-container p-4">
<div className="pb-4">
<h2 className='text-sm leading-5'>{data.TabAuthenticator.Heading}</h2>
<p className='text-xs mt-3 text-disabled-text font-light'>{data.TabAuthenticator.Description}</p>
</div>
<Seperator />
<div className='p-2'>
<div className='mt-14 bg-blue-100 p-4 place-self-center rounded-full'>
<Key size={32} className='stroke-blue-500' />
</div>
<p className='text-sm2 mt-4 font-light place-self-center'>
{data.TabAuthenticator.Tip}
</p>
<div className='place-self-center mt-2'>
<Button className='mt-4 px-14 py-2'>
<div className='flex items-center justify-center gap-1'>
<TickCircle className='stroke-white' size={20} />
<span className='mt-0.5'>
{data.TabAuthenticator.ButtonSubmit}
</span>
</div>
</Button>
</div>
{data.TabAuthenticator.Guides.map((guide, i) => {
return (
<section
key={i}
className='bg-background rounded-normal mt-14 p-4 text-xs flex flex-col gap-2 font-light'
>
<h6 className='text-sm'>{guide.Heading}</h6>
<p className='text-disabled-text'>{guide.Description}</p>
{guide.Items.map((item, i) => {
return (
<div key={i}>
<div className='ml-2 mt-2 bg-blue-100 inline-block size-6 text-center pt-[5px] pr-[1px] text-blue-500 rounded-full'>
{item.Mark}
</div>
<span>
{item.Label}
</span>
</div>
)
})}
{guide.Notes.map((note, i) => {
return (
<div
key={i}
className='mt-4 p-3 text-yellow-800 rounded-lg bg-yellow-50 border border-solid border-yellow-200'
>
{note}
</div>
)
})}
</section>
)
})}
</div>
</div>
</section>
)
}
const handleStatisticalToggle = (checked: boolean) => {
setStatisticalParticipation(checked);
console.log("Statistical participation:", checked);
};
const notifsTab = () => {
return (
<div className='overflow-y-auto h-full noscrollbar flex flex-col gap-6'>
<div className='grid grid-cols-3 items-center'>
<span></span>
<h1 className='text-sm2 place-self-center font-medium'>{data.Heading}</h1>
<ArrowLeft
className='cursor-pointer place-self-end'
size='24'
color='currentColor'
onClick={() => { router.back() }}
/>
<section aria-labelledby="about-title" className="py-4 flex-1 h-full">
<section className={glassSurfaceCard("p-4")}>
<div className="flex justify-between items-center pb-[25px]">
<h2 className="text-sm leading-5">{data.TabNotifications.Heading}</h2>
</div>
<div className="p-2 pt-0">
{data.TabNotifications.Accordions.map((accordion, i) => {
return (
<Fragment key={i}>
<Accordion title={accordion.Heading} group={"accordions"} icon={accordion.Icon}>
<div className="grid gap-4 mb-3">
{accordion.Items.map((item, j) => {
return (
<label
key={j}
htmlFor={item.HtmlName}
className="flex w-full h-11 items-center justify-between gap-2 px-3 py-4 relative bg-container/29 rounded-xl border border-solid border-neutral-200 dark:border-neutral-600 focus:outline-none focus:ring-2 focus:ring-black"
>
<span className="inline-flex items-center gap-2.5 text-sm2">
<SmartIcon icon={item.Icon} />
{item.Label}
</span>
<Switch onClick={onSwitched} id={item.HtmlName} name={item.HtmlName} className="w-12 h-6 scale-[0.9]" />
</label>
);
})}
</div>
</Accordion>
<Seperator className="last:hidden" />
</Fragment>
);
})}
</div>
</section>
</section>
);
};
const passwordTab = () => {
return (
<section aria-labelledby="reviews-title" className="py-4">
<section className={glassSurfaceCard("p-4")}>
<div className="flex justify-between items-center pb-[25px]">
<h2 className="text-sm leading-5">{data.TabPassword.Heading}</h2>
</div>
<form onSubmit={(e) => e.preventDefault()} className="grid gap-6 p-2">
{data.TabPassword.Inputs.map((input, i) => {
return <PasswordField labelText={input.Label} className="bg-container" key={i} htmlFor={input.HtmlName ?? ""} placeholder={input.Placeholder} autoComplete={input.AutoComplete} />;
})}
{data.TabPassword.Rules.map((rule, i) => {
return (
<section key={i} className="bg-background rounded-normal p-4 text-xs flex flex-col gap-2 font-light">
<h6 className="text-sm mb-2">{rule.Heading}</h6>
{rule.Items.map((item, i) => {
return <li key={i}>{item}</li>;
})}
</section>
);
})}
<Button className="mt-4">
<div className="flex items-center justify-center gap-1">
<TickCircle className="stroke-white" size={20} />
<span className="mt-0.5">{data.TabPassword.ButtonSubmit}</span>
</div>
</Button>
</form>
</section>
</section>
);
};
const authenticatorTab = () => {
return (
<section aria-labelledby="reviews-title" className="py-4">
<div className={glassSurfaceCard("p-4")}>
<div className="pb-4">
<h2 className="text-sm leading-5">{data.TabAuthenticator.Heading}</h2>
<p className="text-xs mt-3 text-disabled-text font-light">{data.TabAuthenticator.Description}</p>
</div>
<Seperator />
<div className="p-2">
<div className="mt-14 bg-blue-100 p-4 place-self-center rounded-full">
<Key size={32} className="stroke-blue-500" />
</div>
<p className="text-sm2 mt-4 font-light place-self-center">{data.TabAuthenticator.Tip}</p>
<div className="place-self-center mt-2">
<Button className="mt-4 px-14 py-2">
<div className="flex items-center justify-center gap-1">
<TickCircle className="stroke-white" size={20} />
<span className="mt-0.5">{data.TabAuthenticator.ButtonSubmit}</span>
</div>
</Button>
</div>
<section className='bg-container rounded-container shadow-container p-4'>
<div className='flex justify-between items-center pb-4'>
<h2 className='text-sm leading-5'>{data.StatisticalParticipation.Heading}</h2>
<Switch
checked={statisticalParticipation}
onCheckedChange={handleStatisticalToggle}
id={data.StatisticalParticipation.HtmlName}
name={data.StatisticalParticipation.HtmlName}
className="w-12 h-6 scale-[0.9]"
/>
</div>
<p className='text-xs text-disabled-text font-light'>
{data.StatisticalParticipation.Description}
</p>
</section>
{data.TabAuthenticator.Guides.map((guide, i) => {
return (
<section key={i} className="bg-background rounded-normal mt-14 p-4 text-xs flex flex-col gap-2 font-light">
<h6 className="text-sm">{guide.Heading}</h6>
<p className="text-disabled-text">{guide.Description}</p>
{guide.Items.map((item, i) => {
return (
<div key={i}>
<div className="ml-2 mt-2 bg-blue-100 inline-block size-6 text-center pt-[5px] pr-px text-blue-500 rounded-full">{item.Mark}</div>
<span>{item.Label}</span>
</div>
);
})}
{guide.Notes.map((note, i) => {
return (
<div key={i} className="mt-4 p-3 text-yellow-800 rounded-lg bg-yellow-50 border border-solid border-yellow-200">
{note}
</div>
);
})}
</section>
);
})}
</div>
</div>
)
</section>
);
};
return (
<div className="overflow-y-auto h-full noscrollbar flex flex-col gap-6">
<div className="grid grid-cols-3 items-center">
<span></span>
<h1 className="text-sm2 place-self-center font-medium">{data.Heading}</h1>
<ArrowLeft
className="cursor-pointer place-self-end"
size="24"
color="currentColor"
onClick={() => {
router.back();
}}
/>
</div>
<section className={glassSurfaceCard("p-4")}>
<div className="flex justify-between items-center pb-4">
<h2 className="text-sm leading-5">{data.StatisticalParticipation.Heading}</h2>
<Switch
checked={statisticalParticipation}
onCheckedChange={handleStatisticalToggle}
id={data.StatisticalParticipation.HtmlName}
name={data.StatisticalParticipation.HtmlName}
className="w-12 h-6 scale-[0.9]"
/>
</div>
<p className="text-xs text-disabled-text font-light">{data.StatisticalParticipation.Description}</p>
</section>
</div>
);
}
export default UserSettingsIndex
export default UserSettingsIndex;
+75 -101
View File
@@ -1,111 +1,85 @@
"use client";
import Button from '@/components/button/PrimaryButton';
import InputField from '@/components/input/InputField';
import { ef, ue } from '@/lib/helpers/utfNumbers';
import { ArrowLeft, ArrowRight, WalletMinus } from 'iconsax-react';
import { useRouter } from 'next/navigation';
import React, { useState } from 'react'
import Button from "@/components/button/PrimaryButton";
import InputField from "@/components/input/InputField";
import { ef, ue } from "@/lib/helpers/utfNumbers";
import { glassSurfaceCard } from "@/lib/styles/glassSurface";
import { ArrowLeft, ArrowRight, WalletMinus } from "iconsax-react";
import { useRouter } from "next/navigation";
import React, { useState } from "react";
type Props = object
type Props = object;
function UserWalletIndex({ }: Props) {
const router = useRouter();
const [chargeValue, setChargeValue] = useState<string>('0');
const chargeAction = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const data = new FormData(e.currentTarget)
const value = Number(ue(String(data.get('chargeValue') ?? 0))) + 0;
console.log("Charge for: ", value);
}
function UserWalletIndex({}: Props) {
const router = useRouter();
const [chargeValue, setChargeValue] = useState<string>("0");
const chargeAction = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const data = new FormData(e.currentTarget);
const value = Number(ue(String(data.get("chargeValue") ?? 0))) + 0;
console.log("Charge for: ", value);
};
return (
<div className='overflow-y-auto h-full noscrollbar flex flex-col'>
<div className='grid grid-cols-3 items-center'>
<span></span>
<h1 className='text-sm2 place-self-center font-medium'>پروفایل کاربری</h1>
<ArrowLeft
className='cursor-pointer place-self-end'
size='24'
color='currentColor'
onClick={() => { router.back() }}
/>
return (
<div className="overflow-y-auto h-full noscrollbar flex flex-col">
<div className="grid grid-cols-3 items-center">
<span></span>
<h1 className="text-sm2 place-self-center font-medium">پروفایل کاربری</h1>
<ArrowLeft
className="cursor-pointer place-self-end"
size="24"
color="currentColor"
onClick={() => {
router.back();
}}
/>
</div>
<form onSubmit={chargeAction} className={glassSurfaceCard("mt-8 flex-1 h-full w-full py-6 px-4 flex flex-col justify-between")}>
<div className="">
<div className="flex items-center justify-center text-center gap-4 pb-6 border-b-[1.5px] border-border">
<div className="block">
<div className="font-medium text-sm flex items-center gap-2">
<WalletMinus className="size-5 stroke-foreground mb-0.5" />
موجودی کیف پول
</div>
<div className="text-sm text-primary mt-3 inline-flex dark:text-neutral-200 items-center gap-2">{ef("250.000 تومان")}</div>
</div>
</div>
<form
onSubmit={chargeAction}
className="mt-8 flex-1 h-full bg-container rounded-container w-full box-shadow-normal py-6 px-4 flex flex-col justify-between">
<div className="">
<div className="flex items-center justify-center text-center gap-4 pb-6 border-b-[1.5px] border-border">
<div className='block'>
<div className='font-medium text-sm flex items-center gap-2'>
<WalletMinus className='size-5 stroke-foreground mb-0.5' />
موجودی کیف پول
</div>
<div className='text-sm text-primary mt-3 inline-flex dark:text-neutral-200 items-center gap-2'>
{ef('250.000 تومان')}
</div>
</div>
</div>
<div className="grid gap-3 mt-6">
<div className='inline-flex items-center mb-3 gap-2.5 text-sm2'>
برای شارژ کیف پول خود میتوانید بر روی یکی از مبالغ زیر کلیک کنید و یا مبلغ دلخواه خود را وارد کنید
</div>
<Button
type='button'
onClick={() => setChargeValue('50000')}
className='!bg-disabled text-sm2 !text-foreground flex justify-between items-center'
>
<ArrowRight className='stroke-foreground size-5' />
<span className='mt-0.5'>
{ef('50,000 تومان')}
</span>
</Button>
<Button
type='button'
onClick={() => setChargeValue('150000')}
className='!bg-disabled text-sm2 !text-foreground flex justify-between items-center'
>
<ArrowRight className='stroke-foreground size-5' />
<span className='mt-0.5'>
{ef('150,000 تومان')}
</span>
</Button>
<Button
type='button'
onClick={() => setChargeValue('300000')}
className='!bg-disabled text-sm2 !text-foreground flex justify-between items-center'
>
<ArrowRight className='stroke-foreground size-5' />
<span className='mt-0.5'>
{ef('300,000 تومان')}
</span>
</Button>
<InputField
className='bg-container mt-7'
labelText='مبلغ دلخواه (تومان)'
dir='ltr'
inputMode='decimal'
htmlFor='chargeValue'
placeholder=''
value={ef(chargeValue)}
onChange={(e) => setChargeValue(e.target.value)}
/>
</div>
</div>
<div className='w-full text-center mt-6'>
<Button
type='submit'
>
شارژ کیف پول
</Button>
</div>
</form>
<div className="grid gap-3 mt-6">
<div className="inline-flex items-center mb-3 gap-2.5 text-sm2">برای شارژ کیف پول خود میتوانید بر روی یکی از مبالغ زیر کلیک کنید و یا مبلغ دلخواه خود را وارد کنید</div>
<Button type="button" onClick={() => setChargeValue("50000")} className="bg-disabled! text-sm2 text-foreground! flex justify-between items-center">
<ArrowRight className="stroke-foreground size-5" />
<span className="mt-0.5">{ef("50,000 تومان")}</span>
</Button>
<Button type="button" onClick={() => setChargeValue("150000")} className="bg-disabled! text-sm2 text-foreground! flex justify-between items-center">
<ArrowRight className="stroke-foreground size-5" />
<span className="mt-0.5">{ef("150,000 تومان")}</span>
</Button>
<Button type="button" onClick={() => setChargeValue("300000")} className="bg-disabled! text-sm2 text-foreground! flex justify-between items-center">
<ArrowRight className="stroke-foreground size-5" />
<span className="mt-0.5">{ef("300,000 تومان")}</span>
</Button>
<InputField
className="bg-container mt-7"
labelText="مبلغ دلخواه (تومان)"
dir="ltr"
inputMode="decimal"
htmlFor="chargeValue"
placeholder=""
value={ef(chargeValue)}
onChange={(e) => setChargeValue(e.target.value)}
/>
</div>
</div>
)
<div className="w-full text-center mt-6">
<Button type="submit">شارژ کیف پول</Button>
</div>
</form>
</div>
);
}
export default UserWalletIndex
export default UserWalletIndex;
+8 -1
View File
@@ -1,10 +1,12 @@
"use client";
import ActiveChecker from "@/components/ActiveChecker";
import AppBackground from "@/components/background/AppBackground";
import CartChecker from "@/components/CartChecker";
import RestaurantHeadManager from "@/components/RestaurantHeadManager";
import RestaurantNotFoundGuard from "@/components/RestaurantNotFoundGuard";
import PreferenceWrapper from "@/components/wrapper/PreferenceWrapper";
import { PatternBackgroundProvider } from "@/components/background/PatternBackgroundProvider";
type Props = {
children: React.ReactNode;
@@ -15,7 +17,12 @@ export default function RestaurantLayoutClient({ children }: Props) {
<>
<RestaurantHeadManager />
<RestaurantNotFoundGuard>
<PreferenceWrapper>{children}</PreferenceWrapper>
<PatternBackgroundProvider>
<PreferenceWrapper>
<AppBackground />
{children}
</PreferenceWrapper>
</PatternBackgroundProvider>
</RestaurantNotFoundGuard>
<CartChecker />
<ActiveChecker />
+1 -87
View File
@@ -1,99 +1,13 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { fetchWithTimeout } from "@/lib/helpers/fetchWithTimeout";
import { PWA_ICON_192 } from "@/lib/helpers/pwaIcons";
import { NextRequest, NextResponse } from "next/server";
import sharp from "sharp";
export const dynamic = "force-dynamic";
export const revalidate = 0;
const FETCH_TIMEOUT_MS = 5000;
const DEFAULT_ICON = PWA_ICON_192;
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ name: string }> },
) {
// Restaurant logo processing disabled redirect to static PWA icon
await params;
return NextResponse.redirect(new URL(PWA_ICON_192, request.url), 302);
/* Restaurant logo processing uncomment to re-enable dynamic PWA icons
try {
await params;
const searchParams = request.nextUrl.searchParams;
const logoUrl = searchParams.get("url");
const size = parseInt(searchParams.get("size") || "192");
if (!logoUrl) {
return new NextResponse("Logo URL is required", { status: 400 });
}
const imageResponse = await fetchWithTimeout(logoUrl, {
timeoutMs: FETCH_TIMEOUT_MS,
});
if (!imageResponse.ok) {
return NextResponse.redirect(new URL(DEFAULT_ICON, request.url), 302);
}
const imageBuffer = Buffer.from(await imageResponse.arrayBuffer());
const metadata = await sharp(imageBuffer).metadata();
const { width, height } = metadata;
if (!width || !height) {
return NextResponse.redirect(new URL(DEFAULT_ICON, request.url), 302);
}
if (width === height) {
const processedImage = sharp(imageBuffer).resize(size, size, {
fit: "contain",
background: { r: 0, g: 0, b: 0, alpha: 0 },
});
const outputBuffer: any = await processedImage.png().toBuffer();
return new NextResponse(outputBuffer, {
headers: {
"Content-Type": "image/png",
"Cache-Control": "public, max-age=31536000, immutable",
},
});
}
const maxDimension = Math.max(width, height);
const scale = size / maxDimension;
const newWidth = Math.round(width * scale);
const newHeight = Math.round(height * scale);
const paddingTop = Math.floor((size - newHeight) / 2);
const paddingBottom = size - newHeight - paddingTop;
const paddingLeft = Math.floor((size - newWidth) / 2);
const paddingRight = size - newWidth - paddingLeft;
const processedImage = sharp(imageBuffer)
.resize(newWidth, newHeight, {
fit: "contain",
background: { r: 0, g: 0, b: 0, alpha: 0 },
})
.extend({
top: paddingTop,
bottom: paddingBottom,
left: paddingLeft,
right: paddingRight,
background: { r: 0, g: 0, b: 0, alpha: 0 },
});
const outputBuffer: any = await processedImage.png().toBuffer();
return new NextResponse(outputBuffer, {
headers: {
"Content-Type": "image/png",
"Cache-Control": "public, max-age=31536000, immutable",
},
});
} catch (error) {
console.error("Error processing logo:", error);
return NextResponse.redirect(new URL(DEFAULT_ICON, request.url), 302);
}
*/
}
+1 -1
View File
@@ -17,7 +17,7 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: "Invalid URL" }, { status: 400 });
}
const res = await fetchWithTimeout(url, {
timeoutMs: 5000,
timeoutMs: 15_000,
headers: { Accept: "image/svg+xml, text/xml, text/plain" },
});
if (!res.ok) {
+5
View File
@@ -3,6 +3,7 @@ import {
fetchAboutResponse,
fetchCategoriesResponse,
fetchFoodsResponse,
fetchSlidersResponse,
} from "./serverMenuFetch";
export async function prefetchMenuPageData(
@@ -22,5 +23,9 @@ export async function prefetchMenuPageData(
queryKey: ["categories", name],
queryFn: () => fetchCategoriesResponse(name),
}),
queryClient.prefetchQuery({
queryKey: ["sliders", name],
queryFn: () => fetchSlidersResponse(name),
}),
]);
}
+19 -1
View File
@@ -1,7 +1,11 @@
import axios from "axios";
import { API_BASE_URL } from "@/config/const";
import type { AboutResponse } from "../(Main)/about/types/Types";
import type { CategoriesResponse, FoodsResponse } from "../(Main)/types/Types";
import type {
CategoriesResponse,
FoodsResponse,
SlidersResponse,
} from "../(Main)/types/Types";
import { getRestaurant, RESTAURANT_FETCH_TIMEOUT_MS } from "./getRestaurant";
function createServerAxios(slug: string) {
@@ -37,3 +41,17 @@ export async function fetchCategoriesResponse(
);
return data;
}
export async function fetchSlidersResponse(
slug: string,
): Promise<SlidersResponse> {
try {
const client = createServerAxios(slug);
const { data } = await client.get<SlidersResponse>(
`/public/sliders/restaurant/${slug}`,
);
return data;
} catch {
return { success: true, data: [] };
}
}
-34
View File
@@ -1,34 +0,0 @@
import { readFile } from 'fs/promises';
import path from 'path';
import sharp from 'sharp';
import { NextRequest, NextResponse } from 'next/server';
const VALID_SIZES = [192, 512] as const;
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ size: string }> },
) {
const { size: sizeParam } = await params;
const size = parseInt(sizeParam, 10);
if (!VALID_SIZES.includes(size as (typeof VALID_SIZES)[number])) {
return new NextResponse('Invalid size', { status: 400 });
}
try {
const svgPath = path.join(process.cwd(), 'public', 'icon0.svg');
const svgBuffer = await readFile(svgPath);
const pngBuffer = await sharp(svgBuffer).resize(size, size).png().toBuffer();
return new NextResponse(new Uint8Array(pngBuffer), {
headers: {
'Content-Type': 'image/png',
'Cache-Control': 'public, max-age=31536000, immutable',
},
});
} catch (error) {
console.error('Error generating PWA icon:', error);
return new NextResponse('Failed to generate icon', { status: 500 });
}
}
+72
View File
@@ -0,0 +1,72 @@
"use client";
import { PublicRestaurant } from "@/app/types/Types";
import { glassSurfaceCard } from "@/lib/styles/glassSurface";
import { ArrowLeft2, Location } from "iconsax-react";
import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
const FALLBACK_LOGO = "/assets/images/avatar.svg";
function isValidLogoUrl(url: string | null | undefined): url is string {
if (!url?.trim()) return false;
const lower = url.toLowerCase();
if (lower.endsWith(".pdf")) return false;
return lower.startsWith("http") || lower.startsWith("/");
}
function getInitials(name: string): string {
const trimmed = name.trim();
if (!trimmed) return "?";
return trimmed.charAt(0);
}
type Props = {
restaurant: PublicRestaurant;
};
export default function RestaurantCard({ restaurant }: Props) {
const validLogo = isValidLogoUrl(restaurant.logo);
const [logoSrc, setLogoSrc] = useState<string>(validLogo && restaurant.logo ? restaurant.logo : FALLBACK_LOGO);
const showInitials = !validLogo || logoSrc === FALLBACK_LOGO;
return (
<Link
href={`/${restaurant.slug}`}
className={glassSurfaceCard(
"group flex items-center gap-4 p-4 transition-all duration-200 ease-out",
"hover:shadow-md active:scale-[0.98] outline-none [-webkit-tap-highlight-color:transparent]",
)}
>
<div className="relative shrink-0">
{showInitials ? (
<div className="flex size-16 items-center justify-center rounded-2xl bg-(--danak-orange-light) text-xl font-medium text-(--danak-orange)">{getInitials(restaurant.name)}</div>
) : (
<Image src={logoSrc} alt={restaurant.name} width={64} height={64} className="size-16 rounded-2xl bg-[#F2F2F9] object-cover ring-1 ring-black/5" onError={() => setLogoSrc(FALLBACK_LOGO)} />
)}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-start justify-between gap-2">
<h2 className="truncate text-sm font-medium text-foreground">{restaurant.name}</h2>
</div>
{restaurant.address && (
<div className="mt-1.5 flex items-start gap-1.5 text-[#8C90A3]">
<Location stroke="currentColor" size={14} className="mt-0.5 shrink-0 text-(--danak-orange)" variant="Bold" />
<p className="line-clamp-2 text-xs leading-5">{restaurant.address?.trim()}</p>
</div>
)}
{restaurant.establishedYear != null && (
<p className="mt-1.5 text-[11px] text-[#8C90A3]">
سال تاسیس <span className="font-medium text-(--danak-orange)">{restaurant.establishedYear}</span>
</p>
)}
</div>
<ArrowLeft2 size={18} className="shrink-0 text-[#C4C7D4] transition-transform duration-200 group-hover:-translate-x-0.5 group-hover:text-(--danak-orange)" />
</Link>
);
}
@@ -0,0 +1,24 @@
import { glassSurfaceCard } from "@/lib/styles/glassSurface";
function SkeletonCard() {
return (
<div className={glassSurfaceCard("flex animate-pulse items-center gap-4 p-4")}>
<div className="size-16 shrink-0 rounded-2xl bg-(--danak-orange-muted)" />
<div className="flex-1 space-y-2.5">
<div className="h-4 w-2/3 rounded-lg bg-black/5" />
<div className="h-3 w-full rounded-lg bg-black/5" />
<div className="h-3 w-4/5 rounded-lg bg-black/5" />
</div>
</div>
);
}
export default function RestaurantsSkeleton() {
return (
<div className="space-y-3 px-4 pb-8">
{Array.from({ length: 6 }).map((_, i) => (
<SkeletonCard key={i} />
))}
</div>
);
}
+35
View File
@@ -1,6 +1,7 @@
@import "tailwindcss";
@import "../../public/assets/css/fonts.css";
@import "tw-animate-css";
@import "../styles/glass.css";
/* @custom-variant dark (&:is(.dark *)); */
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
@@ -395,4 +396,38 @@ html[data-theme="dark"] {
background-color: var(--primary);
-webkit-mask: url("/assets/images/fast-delivery.svg") no-repeat center / contain;
mask: url("/assets/images/fast-delivery.svg") no-repeat center / contain;
}
html[data-pattern-bg="true"] #root {
position: relative;
z-index: 1;
}
html[data-pattern-bg="true"] [data-slot="dialog-content"] {
background-color: var(--container) !important;
}
html[data-pattern-bg="true"] [data-slot="splash-screen"] {
background-color: var(--container) !important;
}
html[data-pattern-bg="true"] .pattern-secondary-bg {
background-color: var(--primary-light) !important;
}
html[data-image-bg="true"] #root {
position: relative;
z-index: 1;
}
html[data-image-bg="true"] [data-slot="dialog-content"] {
background-color: var(--container) !important;
}
html[data-image-bg="true"] [data-slot="splash-screen"] {
background-color: var(--container) !important;
}
html[data-image-bg="true"] .pattern-secondary-bg {
background-color: var(--primary-light) !important;
}
+6
View File
@@ -0,0 +1,6 @@
.danak-home {
--danak-orange: #ff5e2b;
--danak-orange-dark: #e84e1c;
--danak-orange-light: rgb(255 94 43 / 12%);
--danak-orange-muted: rgb(255 94 43 / 6%);
}
+9
View File
@@ -0,0 +1,9 @@
import { useQuery } from "@tanstack/react-query";
import * as api from "../service/Service";
export const useGetAllRestaurants = () => {
return useQuery({
queryKey: ["restaurants"],
queryFn: api.getAllRestaurants,
});
};
+2
View File
@@ -7,6 +7,7 @@ import initTranslations from '@/lib/i18n';
import TranslationsProvider from "@/components/utils/TranslationsProdiver";
import ToastContainer from "@/components/Toast";
import { ThemeColorSetter } from "@/components/ThemeColorSetter";
import { ThemeBootScript } from "@/components/ThemeBootScript";
import PwaServiceWorker from "@/components/PwaServiceWorker";
export const metadata: Metadata = {
@@ -48,6 +49,7 @@ export default async function RootLayout({
className={`antialiased bg-background h-svh overflow-hidden`}
>
<ThemeBootScript />
<ReactQueryProvider>
<TranslationsProvider
namespaces={i18nNamespaces}
-10
View File
@@ -1,10 +0,0 @@
import LoadingOverlay from '@/components/overlays/LoadingOverlay'
import React from 'react'
function Loading() {
return (
<LoadingOverlay />
)
}
export default Loading
+73 -12
View File
@@ -1,14 +1,75 @@
import Link from 'next/link'
"use client";
export default function Home () {
return (
<div>
<Link href={'/auth'}>Auth</Link>
<br />
<Link href={'/zhivan'}>Menu</Link>
<br />
<Link href={'/'}>Index</Link>
<br />
</div>
)
import RestaurantCard from "@/app/components/RestaurantCard";
import RestaurantsSkeleton from "@/app/components/RestaurantsSkeleton";
import { useGetAllRestaurants } from "@/app/hooks/useAppData";
import { PublicRestaurant } from "@/app/types/Types";
import SearchBox from "@/components/input/SearchBox";
import Image from "next/image";
import { useMemo, useState } from "react";
import "./home.css";
function filterRestaurants(restaurants: PublicRestaurant[], query: string): PublicRestaurant[] {
const normalized = query.trim().toLowerCase();
if (!normalized) return restaurants;
return restaurants.filter((r) => r.name.toLowerCase().includes(normalized) || r.slug.toLowerCase().includes(normalized) || r.address?.toLowerCase().includes(normalized));
}
export default function Home() {
const { data, isLoading, error } = useGetAllRestaurants();
const [search, setSearch] = useState("");
const restaurants = data?.data ?? [];
const filtered = useMemo(() => filterRestaurants(restaurants, search), [restaurants, search]);
return (
<div className="danak-home flex h-full flex-col overflow-hidden bg-[#ff5e2b]/20">
<header className="shrink-0 px-4 pt-6 pb-3">
<div className="flex flex-col items-center text-center">
<Image src="/icons/logo512.png" alt="BY DANAK" width={52} height={52} className="size-[52px] rounded-[14px] object-cover" priority />
<h1 className="mt-3 text-base font-semibold text-foreground">دیمنو پلاس</h1>
<p className="mt-0.5 text-[11px] tracking-wide text-(--danak-orange)">BY DANAK</p>
</div>
<div className="mt-5">
<SearchBox value={search} onChange={(e) => setSearch(e.target.value)} placeholder="جستجوی رستوران یا کافه..." />
</div>
</header>
<main className="min-h-0 flex-1 overflow-y-auto noscrollbar">
{!isLoading && !error && (
<p className="px-4 pb-3 text-xs text-[#8C90A3]">
{filtered.length.toLocaleString("fa-IR")} مورد
{search.trim() ? " یافت شد" : ""}
</p>
)}
{isLoading && <RestaurantsSkeleton />}
{error && (
<div className="mx-4 rounded-2xl border border-red-200 bg-red-50 px-4 py-6 text-center">
<p className="text-sm font-medium text-red-700">خطا در دریافت لیست رستورانها</p>
<p className="mt-1 text-xs text-red-500">لطفاً اتصال اینترنت خود را بررسی کنید و دوباره تلاش کنید.</p>
</div>
)}
{!isLoading && !error && filtered.length === 0 && (
<div className="flex flex-col items-center justify-center px-4 py-16 text-center">
<Image src="/icons/logo512.png" alt="" width={48} height={48} className="mb-4 size-12 rounded-xl object-cover opacity-40" />
<p className="text-sm font-medium text-foreground">{search.trim() ? "نتیجه‌ای یافت نشد" : "رستورانی یافت نشد"}</p>
<p className="mt-1 text-xs text-[#8C90A3]">{search.trim() ? "عبارت جستجو را تغییر دهید" : "در حال حاضر رستورانی در لیست وجود ندارد"}</p>
</div>
)}
{!isLoading && !error && filtered.length > 0 && (
<div className="space-y-3 px-4 pb-8">
{filtered.map((restaurant) => (
<RestaurantCard key={restaurant.slug} restaurant={restaurant} />
))}
</div>
)}
</main>
</div>
);
}
+7
View File
@@ -0,0 +1,7 @@
import { RestaurantsListResponse } from "@/app/types/Types";
import { api } from "@/config/axios";
export const getAllRestaurants = async (): Promise<RestaurantsListResponse> => {
const { data } = await api.get("/public/restaurants");
return data;
};
+11
View File
@@ -0,0 +1,11 @@
import { BaseResponse } from "@/app/[name]/(Main)/types/Types";
export interface PublicRestaurant {
slug: string;
name: string;
address: string | null;
logo: string | null;
establishedYear: number | null;
}
export type RestaurantsListResponse = BaseResponse<PublicRestaurant[]>;
+10
View File
@@ -0,0 +1,10 @@
import { getThemeBootScript } from "@/lib/helpers/themeBootScript";
export function ThemeBootScript() {
return (
<script
id="theme-boot"
dangerouslySetInnerHTML={{ __html: getThemeBootScript() }}
/>
);
}
+11 -3
View File
@@ -1,12 +1,20 @@
'use client'
import { useEffect } from 'react'
import { useLayoutEffect } from 'react'
import { hexToOklch, calculateBrightness } from '@/lib/helpers/colorUtils'
import { getCachedMenuColor, getRestaurantSlugFromPath, getThemeCache, applyCachedThemeToDom } from '@/lib/helpers/themeCache'
export function ThemeColorSetter() {
useEffect(() => {
useLayoutEffect(() => {
try {
const savedColor = localStorage.getItem('theme-primary-color')
const slug = getRestaurantSlugFromPath()
const cachedTheme = slug ? getThemeCache(slug) : null
if (cachedTheme) {
applyCachedThemeToDom(cachedTheme)
return
}
const savedColor = getCachedMenuColor(slug)
if (savedColor) {
const oklchColor = hexToOklch(savedColor)
@@ -0,0 +1,83 @@
"use client";
import { usePatternBackground } from "@/components/background/PatternBackgroundProvider";
import usePreference from "@/hooks/helpers/usePreference";
import { THEME_BG_ELEMENT_ID } from "@/lib/helpers/themeCache";
import { useLayoutEffect } from "react";
const THEME_BG_IMG_ID = "cached-theme-bg-img";
const THEME_BG_OVERLAY_ID = "cached-theme-bg-overlay";
export default function AppBackground() {
const { state: nightMode } = usePreference("night-mode", false);
const { isPattern, backgroundStyle, isCustomImage, customImageSettings } =
usePatternBackground();
useLayoutEffect(() => {
if (nightMode || (!isPattern && !isCustomImage)) {
document.getElementById(THEME_BG_ELEMENT_ID)?.remove();
return;
}
let element = document.getElementById(THEME_BG_ELEMENT_ID);
if (!element) {
element = document.createElement("div");
element.id = THEME_BG_ELEMENT_ID;
element.setAttribute("aria-hidden", "true");
element.style.position = "fixed";
element.style.inset = "0";
element.style.pointerEvents = "none";
element.style.zIndex = "0";
document.body.appendChild(element);
}
if (isPattern) {
element.style.overflow = "";
document.getElementById(THEME_BG_IMG_ID)?.remove();
document.getElementById(THEME_BG_OVERLAY_ID)?.remove();
Object.assign(element.style, backgroundStyle as Record<string, string>);
return;
}
if (isCustomImage && customImageSettings) {
const { bgUrl, bgBlur, bgOpacity, bgOverlay } = customImageSettings;
element.style.backgroundColor = "";
element.style.backgroundImage = "";
element.style.overflow = "hidden";
let imgLayer = document.getElementById(THEME_BG_IMG_ID);
if (!imgLayer) {
imgLayer = document.createElement("div");
imgLayer.id = THEME_BG_IMG_ID;
imgLayer.style.position = "absolute";
imgLayer.style.inset = "-40px";
imgLayer.style.backgroundSize = "cover";
imgLayer.style.backgroundPosition = "center";
imgLayer.style.backgroundRepeat = "no-repeat";
element.appendChild(imgLayer);
}
imgLayer.style.backgroundImage = `url("${bgUrl}")`;
imgLayer.style.filter = bgBlur ? `blur(${bgBlur}px)` : "";
imgLayer.style.opacity =
bgOpacity != null ? String(bgOpacity / 100) : "1";
let overlayLayer = document.getElementById(THEME_BG_OVERLAY_ID);
if (!overlayLayer) {
overlayLayer = document.createElement("div");
overlayLayer.id = THEME_BG_OVERLAY_ID;
overlayLayer.style.position = "absolute";
overlayLayer.style.inset = "0";
element.appendChild(overlayLayer);
}
const overlayOpacity =
bgOverlay != null ? Number(bgOverlay) / 100 : 0;
overlayLayer.style.backgroundColor =
overlayOpacity > 0 ? `rgba(0,0,0,${overlayOpacity})` : "";
}
}, [backgroundStyle, isCustomImage, isPattern, customImageSettings, nightMode]);
return null;
}
@@ -0,0 +1,250 @@
"use client";
import { useGetAbout } from "@/app/[name]/(Main)/about/hooks/useAboutData";
import {
applyColorBackgroundCssVariables,
applyCustomImageCssVariables,
applyPatternCssVariables,
clearCustomImageCssVariables,
clearPatternCssVariables,
colorizeSvg,
hexToRgba,
normalizeBgNumber,
PATTERN_BACKGROUND_OPACITY,
resolveBgType,
} from "@/lib/helpers/backgroundUtils";
import usePreference from "@/hooks/helpers/usePreference";
import {
applyCachedThemeToDom,
buildThemeCacheFromRestaurant,
clearRestaurantBackgroundOverrides,
getThemeCache,
removeCachedThemeBackground,
setThemeCache,
svgToDataUrl,
themeBackgroundMatches,
type CachedRestaurantTheme,
} from "@/lib/helpers/themeCache";
import { useParams } from "next/navigation";
import {
createContext,
useContext,
useEffect,
useLayoutEffect,
useMemo,
useState,
type CSSProperties,
type ReactNode,
} from "react";
const DEFAULT_MENU_COLOR = "#1E3A8A";
export type CustomImageSettings = {
bgUrl: string;
bgBlur: number | null;
bgOpacity: number | null;
bgOverlay: string | null;
};
type PatternBackgroundContextValue = {
isPattern: boolean;
isCustomImage: boolean;
backgroundStyle: CSSProperties;
customImageSettings: CustomImageSettings | null;
};
const PatternBackgroundContext = createContext<PatternBackgroundContextValue>({
isPattern: false,
isCustomImage: false,
backgroundStyle: {},
customImageSettings: null,
});
export function PatternBackgroundProvider({ children }: { children: ReactNode }) {
const { name: restaurantSlug } = useParams<{ name: string }>();
const { state: nightMode } = usePreference("night-mode", false);
const { data: aboutData } = useGetAbout();
const restaurant = aboutData?.data;
const [hydratedCache, setHydratedCache] = useState<CachedRestaurantTheme | null>(null);
const [patternImageUrl, setPatternImageUrl] = useState<string | null>(null);
const bgType = restaurant
? resolveBgType(restaurant)
: hydratedCache
? resolveBgType(hydratedCache)
: "color";
const isPattern = !nightMode && bgType === "pattern";
const isCustomImage = !nightMode && bgType === "custom";
const menuColor =
restaurant?.menuColor || hydratedCache?.menuColor || DEFAULT_MENU_COLOR;
const baseTint = hexToRgba(menuColor, PATTERN_BACKGROUND_OPACITY);
useLayoutEffect(() => {
if (!restaurantSlug) return;
const cached = getThemeCache(restaurantSlug);
setHydratedCache(cached);
if (!cached) return;
applyCachedThemeToDom(cached);
if (!nightMode && cached.patternDataUrl) {
setPatternImageUrl(cached.patternDataUrl);
}
}, [nightMode, restaurantSlug]);
useEffect(() => {
if (!restaurantSlug) {
clearPatternCssVariables();
clearCustomImageCssVariables();
removeCachedThemeBackground();
setPatternImageUrl(null);
return;
}
if (nightMode) {
clearPatternCssVariables();
clearCustomImageCssVariables();
clearRestaurantBackgroundOverrides();
setPatternImageUrl(null);
return;
}
if (!restaurant) return;
const themeBase = buildThemeCacheFromRestaurant(restaurant);
const currentBgType = themeBase.bgType;
if (currentBgType === "color") {
clearCustomImageCssVariables();
removeCachedThemeBackground();
setPatternImageUrl(null);
applyColorBackgroundCssVariables(menuColor);
setThemeCache(restaurantSlug, { ...themeBase, patternDataUrl: null });
return;
}
if (currentBgType === "custom") {
clearPatternCssVariables();
setPatternImageUrl(null);
applyCustomImageCssVariables();
setThemeCache(restaurantSlug, { ...themeBase, patternDataUrl: null });
return;
}
clearCustomImageCssVariables();
applyPatternCssVariables(menuColor);
if (!themeBase.bgUrl) {
setPatternImageUrl(null);
setThemeCache(restaurantSlug, {
...themeBase,
patternDataUrl: null,
});
const saved = getThemeCache(restaurantSlug);
if (saved) {
applyCachedThemeToDom(saved);
}
return;
}
const cached = getThemeCache(restaurantSlug);
const cacheHit =
cached &&
themeBackgroundMatches(cached, restaurant) &&
cached.isPattern &&
cached.patternDataUrl;
if (cacheHit) {
// Serve cached pattern immediately, then revalidate in background.
setPatternImageUrl(cached.patternDataUrl!);
setThemeCache(restaurantSlug, {
...themeBase,
patternDataUrl: cached.patternDataUrl,
});
applyCachedThemeToDom(cached);
}
let cancelled = false;
const proxyUrl = `/${restaurantSlug}/api/proxy-svg?url=${encodeURIComponent(themeBase.bgUrl)}`;
fetch(proxyUrl, { cache: "no-store" })
.then((response) => {
if (!response.ok) {
throw new Error("Failed to fetch background pattern");
}
return response.text();
})
.then((svg) => {
if (cancelled) return;
const dataUrl = svgToDataUrl(colorizeSvg(svg, menuColor));
if (dataUrl !== cached?.patternDataUrl) {
setPatternImageUrl(dataUrl);
}
const nextCache = {
...themeBase,
patternDataUrl: dataUrl,
};
setThemeCache(restaurantSlug, nextCache);
const saved = getThemeCache(restaurantSlug);
if (saved) {
applyCachedThemeToDom(saved);
}
})
.catch(() => {
if (!cancelled && cached?.patternDataUrl) {
setPatternImageUrl(cached.patternDataUrl);
applyCachedThemeToDom(cached);
}
});
return () => {
cancelled = true;
};
}, [menuColor, nightMode, restaurant, restaurantSlug]);
const backgroundStyle = useMemo<CSSProperties>(
() => ({
backgroundColor: baseTint,
...(patternImageUrl
? {
backgroundImage: `url("${patternImageUrl}")`,
backgroundRepeat: "repeat",
backgroundSize: "auto",
}
: {}),
}),
[baseTint, patternImageUrl],
);
const customImageSettings = useMemo<CustomImageSettings | null>(() => {
const src = restaurant?.bgUrl || hydratedCache?.bgUrl;
if (!isCustomImage || !src) return null;
return {
bgUrl: src,
bgBlur: normalizeBgNumber(restaurant?.bgBlur ?? hydratedCache?.bgBlur),
bgOpacity: normalizeBgNumber(restaurant?.bgOpacity ?? hydratedCache?.bgOpacity),
bgOverlay: restaurant?.bgOverlay ?? hydratedCache?.bgOverlay ?? null,
};
}, [isCustomImage, restaurant, hydratedCache]);
const value = useMemo(
() => ({ isPattern, isCustomImage, backgroundStyle, customImageSettings }),
[backgroundStyle, isPattern, isCustomImage, customImageSettings],
);
return (
<PatternBackgroundContext.Provider value={value}>
{children}
</PatternBackgroundContext.Provider>
);
}
export function usePatternBackground() {
return useContext(PatternBackgroundContext);
}
+124 -60
View File
@@ -4,7 +4,8 @@ import clsx from "clsx";
import { motion, Variants } from "framer-motion";
import { Icon, SearchNormal } from "iconsax-react";
import { ChevronDown } from "lucide-react";
import React, { useEffect, useRef, useState } from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
export interface ComboboxOption {
id: string;
@@ -28,6 +29,12 @@ type Props = {
) => void;
} & React.HTMLAttributes<HTMLDivElement>;
type DropdownPosition = {
top: number;
left: number;
width: number;
};
function Combobox({
title,
options,
@@ -37,17 +44,53 @@ function Combobox({
selectedId,
searchable = true,
onSelectionChange,
className,
...props
}: Props) {
const [expand, setExpand] = useState(expanded);
const [searchValue, setSearchValue] = useState("");
const [dropdownPosition, setDropdownPosition] = useState<DropdownPosition | null>(null);
const boxRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLDivElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const updateDropdownPosition = useCallback(() => {
if (!triggerRef.current) return;
const rect = triggerRef.current.getBoundingClientRect();
setDropdownPosition({
top: rect.bottom + 4,
left: rect.left,
width: rect.width,
});
}, []);
useEffect(() => {
if (!expand) {
setDropdownPosition(null);
return;
}
updateDropdownPosition();
window.addEventListener("scroll", updateDropdownPosition, true);
window.addEventListener("resize", updateDropdownPosition);
return () => {
window.removeEventListener("scroll", updateDropdownPosition, true);
window.removeEventListener("resize", updateDropdownPosition);
};
}, [expand, updateDropdownPosition]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (boxRef.current && !boxRef.current.contains(event.target as Node)) {
setExpand(false);
const target = event.target as Node;
if (
boxRef.current?.contains(target) ||
dropdownRef.current?.contains(target)
) {
return;
}
setExpand(false);
};
if (expand) {
@@ -72,12 +115,14 @@ function Combobox({
e: React.MouseEvent<HTMLDivElement, MouseEvent>,
index: number,
) => {
e.stopPropagation(); // prevent toggle
e.stopPropagation();
onSelectionChange(e, index);
setExpand(false);
};
const selectedOption = options.find((x) => x.id === selectedId);
const filteredOptions = options.filter((v) => v.title?.includes(searchValue));
const activeIcon =
selectedOption?.icon &&
React.createElement(selectedOption.icon, {
@@ -93,7 +138,7 @@ function Combobox({
collapse: {
opacity: 0,
scale: 0.95,
pointerEvents: "none", // disable clicks
pointerEvents: "none",
transition: {
duration: 0.1,
ease: "easeInOut",
@@ -102,7 +147,7 @@ function Combobox({
expand: {
opacity: 1,
scale: 1,
pointerEvents: "auto", // re-enable clicks
pointerEvents: "auto",
transition: {
duration: 0.1,
ease: "easeInOut",
@@ -110,13 +155,84 @@ function Combobox({
},
};
const dropdown =
expand &&
dropdownPosition &&
typeof document !== "undefined"
? createPortal(
<motion.div
ref={dropdownRef}
data-expand={expand}
aria-hidden={!expand}
className="fixed text-foreground bg-container rounded-xl border border-border shadow-lg z-9999"
style={{
top: dropdownPosition.top,
left: dropdownPosition.left,
width: dropdownPosition.width,
}}
role="listbox"
aria-label={`${title} options`}
onClick={(e) => e.stopPropagation()}
initial="collapse"
animate="expand"
variants={variants}
>
{searchable && (
<div className="w-full flex gap-2 border-b border-border px-3 items-center">
<SearchNormal size={16} className="stroke-gray-400" />
<input
placeholder="جستجو ..."
className="w-full outline-none text-sm2 pb-2 pt-3"
onChange={searchChanged}
value={searchValue}
/>
</div>
)}
<div className="flex flex-col p-1">
{filteredOptions.map((v) => {
const originalIndex = options.findIndex((o) => o.id === v.id);
return (
<div
onClick={(e) => setSelection(e, originalIndex)}
key={v.id}
data-selected={v.id === selectedId}
className="flex gap-3 items-center justify-end px-2 py-2 cursor-pointer hover:bg-current/10 rounded-md last:rounded-b-xl"
tabIndex={0}
role="option"
aria-selected={v.id === selectedId}
>
{v?.imagePath ? (
<span className="icon-delivery shrink-0" aria-hidden />
) : (
v?.icon &&
React.createElement(v.icon, {
size: 16,
color: "currentColor",
className:
"inline-block shrink-0 text-primary dark:text-foreground",
})
)}
<span className="text-sm2 tracking-[0.13px] leading-5 flex-1 text-right">
{v.title}
</span>
</div>
);
})}
</div>
</motion.div>,
document.body,
)
: null;
return (
<div
ref={boxRef}
className={clsx("relative", props.className ?? "")}
className={clsx("relative", className)}
{...props}
>
<div
ref={triggerRef}
className="flex w-full h-11 items-center justify-end gap-2 px-3 py-4 relative bg-container/29 rounded-xl border border-solid border-border cursor-pointer"
tabIndex={0}
onClick={toggleExpand}
@@ -163,59 +279,7 @@ function Combobox({
/>
</div>
<motion.div
data-expand={expand}
aria-hidden={!expand}
className="absolute top-full left-0 w-full mt-1 text-foreground bg-container rounded-xl outline outline-solid outline-border shadow-lg z-10"
role="listbox"
aria-label={`${title} options`}
onClick={(e) => e.stopPropagation()}
initial="collapse"
animate={expand ? "expand" : "collapse"}
variants={variants}
>
{searchable && (
<div className="w-full flex gap-2 border-b border-border px-3 items-center">
<SearchNormal size={16} className="stroke-gray-400" />
<input
placeholder="جستجو ..."
className="w-full outline-none text-sm2 pb-2 pt-3"
onChange={searchChanged}
value={searchValue}
/>
</div>
)}
<div className="p-1">
{options
.filter((v) => v.title?.includes(searchValue))
.map((v, i) => (
<div
onClick={(e) => setSelection(e, i)}
key={v.id}
data-selected={v.id === selectedId}
className="flex gap-3 items-center justify-end px-2 py-1 cursor-pointer hover:bg-current/10 rounded-md last:rounded-b-xl"
tabIndex={0}
role="option"
aria-selected
>
{v?.imagePath ? (
<span className="icon-delivery mr-1" aria-hidden />
) : (
v?.icon &&
React.createElement(v.icon, {
size: 16,
color: "currentColor",
className:
"inline-block mr-1 text-primary dark:text-foreground",
})
)}
<span className="text-sm2 tracking-[0.13px] leading-5 w-full mt-1">
{v.title}
</span>
</div>
))}
</div>
</motion.div>
{dropdown}
</div>
);
}
+30 -40
View File
@@ -1,48 +1,38 @@
'use client';
"use client";
import React from 'react'
import Tooltip from '../utils/Tooltip';
import clsx from 'clsx';
import clsx from "clsx";
import React from "react";
import Tooltip from "../utils/Tooltip";
type Props = {
htmlFor: string;
labelText?: React.ReactNode;
value?: string;
valid?: boolean
inputClassName?: string;
htmlFor: string;
labelText?: React.ReactNode;
value?: string;
valid?: boolean;
inputClassName?: string;
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, "id">;
const InputField = React.forwardRef<HTMLInputElement, Props>(({ onChange, htmlFor, labelText, children, valid = true, className, inputClassName, ...inputProps }, ref) => {
return (
<div spellCheck={false} className={`${className} ${valid ? "border-border" : "border-invalid"} h-11 inline-flex relative border px-3 w-full rounded-normal group focus-within:border`}>
<label className="absolute start-2 -top-2.5 px-2 text-[12px] text-foreground bg-container" htmlFor={htmlFor}>
{labelText}
</label>
const InputField = React.forwardRef<HTMLInputElement, Props>(
({ onChange, htmlFor, labelText, children, valid = true, className, inputClassName, ...inputProps }, ref) => {
return (
<div
spellCheck={false}
className={`${className} ${valid ? 'border-border' : 'border-invalid'} h-11 inline-flex relative border px-3 w-full rounded-normal group focus-within:border`}>
<label
className='absolute start-2 -top-2.5 px-2 text-[12px] text-foreground bg-container'
htmlFor={htmlFor}>
{labelText}
</label>
<Tooltip content={inputProps["aria-errormessage"]} hidden={!inputProps["aria-errormessage"]}>
<input
ref={ref}
onChange={onChange}
className={clsx(inputProps["aria-errormessage"] && "text-red-300!", "py-2.5 pt-3.5 text-sm2 w-full outline-0 leading-6!", inputClassName)}
name={htmlFor}
{...inputProps}
/>
{children}
</Tooltip>
</div>
);
});
<Tooltip content={inputProps['aria-errormessage']} hidden={!inputProps['aria-errormessage']}>
<input
ref={ref}
onChange={onChange}
className={clsx(
inputProps['aria-errormessage'] && 'text-red-300!',
'py-2.5 pt-3.5 text-sm2 w-full outline-0 leading-6!',
inputClassName
)}
name={htmlFor}
{...inputProps} />
{children}
</Tooltip>
</div>
)
}
)
InputField.displayName = "InputField";
InputField.displayName = 'InputField'
export default InputField
export default InputField;
+32 -30
View File
@@ -1,38 +1,40 @@
'use client'
"use client";
import React from 'react'
import SearchIcon from '../icons/SearchIcon'
import clsx from 'clsx'
import { useGetAbout } from "@/app/[name]/(Main)/about/hooks/useAboutData";
import { glassSurfaceFlat } from "@/lib/styles/glassSurface";
import Link from "next/link";
import { useParams } from "next/navigation";
import React from "react";
import SearchIcon from "../icons/SearchIcon";
interface SearchboxProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'results'> {
placeholder?: string
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
interface SearchboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "results"> {
placeholder?: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
export default function SearchBox({
className,
value,
placeholder = '',
onChange,
...props
}: SearchboxProps) {
export default function SearchBox({ className, value, placeholder = "", onChange, ...props }: SearchboxProps) {
const { name } = useParams<{ name?: string }>();
const { data: about } = useGetAbout();
const showAiLink = !!name && !!about?.data?.enableAiChat;
return (
<div
className={clsx(
'bg-container inline-flex rounded-xl px-4 h-10 w-full items-center content-center',
className
<div className="flex gap-4 items-center">
<div className={glassSurfaceFlat("inline-flex rounded-xl px-4 h-10 w-full items-center content-center", className)}>
<SearchIcon stroke="#8C90A3" />
<input
onChange={onChange}
value={value}
placeholder={placeholder}
className="text-[#8C90A3] pt-1 block outline-none border-none focus:ring-0 focus:outline-none px-1.75 h-full items-center w-full text-xs!"
type="search"
{...props}
/>
</div>
{showAiLink && (
<Link href={`/${name}/ai`} aria-label="AI" className={glassSurfaceFlat("inline-flex items-center justify-center rounded-xl w-10 h-10 shrink-0")}>
<img src={"/assets/images/logo_ai2.svg"} alt="logo_ai" className="w-5" />
</Link>
)}
>
<SearchIcon stroke='#8C90A3' />
<input
onChange={onChange}
value={value}
placeholder={placeholder}
className='text-[#8C90A3] pt-1 block outline-none border-none focus:ring-0 focus:outline-none px-[7px] h-full items-center w-full text-xs!'
type='search'
{...props}
/>
</div>
)
);
}
@@ -1,5 +1,6 @@
'use client';
import { glassSurface } from '@/lib/styles/glassSurface';
import React from 'react';
type Props = {
@@ -8,10 +9,10 @@ type Props = {
function CategoryItemRenderer({ children, ...rest }: Props) {
return (
<div className={`${rest.className} cursor-pointer transition-all duration-200 ease-out gradient-border overflow-hidden rounded-xl backdrop-blur-md bg-container/40`}>
<div className={glassSurface(rest.className, 'cursor-pointer transition-[background-color,box-shadow,opacity] duration-200 ease-out rounded-xl outline-none [-webkit-tap-highlight-color:transparent]')}>
<div
{...rest}
className="rounded-normal w-[88px] h-[88px] flex flex-col justify-center items-center gap-2"
className="rounded-normal w-[108px] h-[88px] flex flex-col justify-center items-center gap-2 overflow-hidden"
>
{children}
</div>
@@ -1,5 +1,6 @@
'use client';
import { glassSurface } from '@/lib/styles/glassSurface';
import React from 'react';
type Props = {
@@ -8,10 +9,10 @@ type Props = {
function CategorySmallItemRenderer({ children, ...rest }: Props) {
return (
<div className={`${rest.className} cursor-pointer transition-all duration-200 ease-out gradient-border overflow-hidden rounded-xl backdrop-blur-md bg-container/40 dark:bg-background`}>
<div className={glassSurface(rest.className, 'cursor-pointer transition-[background-color,box-shadow,opacity] duration-200 ease-out rounded-xl outline-none [-webkit-tap-highlight-color:transparent]')}>
<div
{...rest}
className="rounded-normal h-[44px] flex flex-row justify-center items-center p-2.5 gap-2"
className="rounded-normal h-[44px] flex flex-row justify-center items-center p-2.5 gap-2 overflow-hidden"
>
{children}
</div>
+23 -61
View File
@@ -51,9 +51,7 @@ const MenuItem = ({ food }: MenuItemProps) => {
if (!content) return "";
if (Array.isArray(content)) {
return content
.filter((item) => item && typeof item === "string")
.join("، ");
return content.filter((item) => item && typeof item === "string").join("، ");
}
return "";
@@ -74,19 +72,16 @@ const MenuItem = ({ food }: MenuItemProps) => {
return basePrice;
}, [food.price, food.discount]);
const formattedPrice = useMemo(
() => (finalPrice ? finalPrice.toLocaleString("fa-IR") : "0"),
[finalPrice],
);
const formattedPrice = useMemo(() => (finalPrice ? finalPrice.toLocaleString("fa-IR") : "0"), [finalPrice]);
const formattedOriginalPrice = useMemo(
() => (food.price ? food.price.toLocaleString("fa-IR") : "0"),
[food.price],
);
const formattedOriginalPrice = useMemo(() => (food.price ? food.price.toLocaleString("fa-IR") : "0"), [food.price]);
const hasDiscount = useMemo(() => (food.discount || 0) > 0, [food.discount]);
const isAvailable = food.isAvailable !== false;
const handleAddToCart = () => {
if (!isAvailable) return;
addToCart(food.id);
};
@@ -99,81 +94,48 @@ const MenuItem = ({ food }: MenuItemProps) => {
return (
<div className="flex gap-4 w-full h-full items-center">
<Link href={foodDetailUrl} className="cursor-pointer">
<img
className="rounded-xl bg-[#F2F2F9] min-w-28 w-28 max-h-28 object-cover"
src={imageSrc}
height={112}
width={112}
alt={foodName}
loading="lazy"
onError={() => setImageSrc(fallbackImage)}
/>
<img className="rounded-xl bg-[#F2F2F9] min-w-28 w-28 max-h-28 object-cover" src={imageSrc} height={112} width={112} alt={foodName} loading="lazy" onError={() => setImageSrc(fallbackImage)} />
</Link>
<div className="w-full inline-flex flex-col justify-between min-w-0">
<Link href={foodDetailUrl} className="cursor-pointer min-w-0">
<div className="min-w-0">
<div className="text-sm2 font-normal text-black dark:text-white wrap-break-word">
{foodName}
</div>
{(foodContent || foodDescription) && (
<div className="text-[#7F7F7F] line-clamp-2 text-xs leading-5 font-normal mt-2 wrap-break-word overflow-hidden">
{foodContent || foodDescription}
</div>
)}
<div className="text-sm2 font-normal text-black dark:text-white wrap-break-word">{foodName}</div>
{(foodContent || foodDescription) && <div className="text-[#7F7F7F] line-clamp-2 text-xs leading-5 font-normal mt-2 wrap-break-word overflow-hidden">{foodContent || foodDescription}</div>}
</div>
</Link>
<div className="inline-flex mt-2 gap-2 justify-between w-full items-center">
<div className="w-full flex flex-col gap-1" dir="ltr">
<div className="inline-flex mt-2 gap-2 justify-between w-full items-start">
<div className="w-full flex flex-col gap-1 items-end" dir="ltr">
{hasDiscount ? (
<>
<span className="text-xs text-disabled-text line-through">
{formattedOriginalPrice} T
</span>
<span className="text-sm font-medium text-primary dark:text-foreground">
{formattedPrice} T
</span>
<span className="text-xs text-disabled-text line-through">{formattedOriginalPrice} T</span>
<span className="text-sm font-medium text-black dark:text-white">{formattedPrice} T</span>
</>
) : (
<span className="text-sm mt-1 text-right">
{formattedPrice} T
</span>
<span className="text-sm text-right">{formattedPrice} T</span>
)}
</div>
<motion.div
whileTap={{ scale: 1.05 }}
className="bg-background active:drop-shadow-xs max-w-[115px] rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2 relative overflow-hidden"
whileTap={isAvailable ? { scale: 1.05 } : undefined}
className="bg-background pattern-secondary-bg active:drop-shadow-xs max-w-28.75 rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2 relative overflow-hidden"
>
{quantity <= 0 ? (
<button
onClick={handleAddToCart}
className="inline-flex w-full justify-center items-center gap-2"
>
<PlusIcon />
<div className="text-sm2 pt-0.5 font-normal text-foreground">
افزودن
</div>
<button onClick={handleAddToCart} disabled={!isAvailable} className="inline-flex w-full justify-center items-center gap-2 disabled:pointer-events-none">
{isAvailable && <PlusIcon />}
<div className="text-sm2 pt-0.5 font-normal text-foreground">{isAvailable ? "افزودن" : "عدم موجودی"}</div>
</button>
) : (
<>
<button
onClick={handleAddToCart}
className="bg-container hover:bg-container/60 active:bg-container/30 rounded-sm p-2 transition-colors"
disabled={!isAvailable}
className="bg-container hover:bg-container/60 active:bg-container/30 rounded-sm p-2 transition-colors disabled:pointer-events-none"
>
<PlusIcon className="text-foreground" />
</button>
<motion.div
key={quantity}
initial={{ scale: 1 }}
animate={{ scale: [1.2, 0.95, 1] }}
transition={{ duration: 0.3 }}
className="text-sm2 pt-0.5 font-semibold"
>
<motion.div key={quantity} initial={{ scale: 1 }} animate={{ scale: [1.2, 0.95, 1] }} transition={{ duration: 0.3 }} className="text-sm2 pt-0.5 font-semibold">
{quantity}
</motion.div>
<button
onClick={handleRemoveFromCart}
className="bg-container hover:bg-container/60 active:bg-container/30 rounded-sm p-2 transition-colors"
>
<button onClick={handleRemoveFromCart} className="bg-container hover:bg-container/60 active:bg-container/30 rounded-sm p-2 transition-colors">
<MinusIcon className="text-foreground" />
</button>
</>
+4 -6
View File
@@ -1,6 +1,7 @@
'use client';
"use client";
import React from 'react';
import { glassSurface } from "@/lib/styles/glassSurface";
import React from "react";
type Props = {
children: React.ReactNode;
@@ -8,10 +9,7 @@ type Props = {
function MenuItemRendererComponent({ children, ...rest }: Props) {
return (
<div
{...rest}
className={`${rest.className} box-shadow-normal !pb-4 py-4 transition-all duration-200 ease-out w-full bg-container rounded-3xl px-4 flex items-center justify-between gap-4`}>
<div {...rest} className={glassSurface(rest.className, "pb-4! py-4 transition-all duration-200 ease-out w-full rounded-3xl px-4 flex items-center justify-between gap-4")}>
{children}
</div>
);

Some files were not shown because too many files have changed in this diff Show More