From 2c6c8ad6e10036dadda4a57bf3e9e1d3558c846a Mon Sep 17 00:00:00 2001 From: Mohammadali Date: Sat, 29 Aug 2026 07:45:53 +0330 Subject: [PATCH 01/12] imp profile new design & wallet new design --- package-lock.json | 149 ++---------------- .../profile/(routes)/(landing)/page.tsx | 33 ++++ .../(shop)/profile/(routes)/wallet/page.tsx | 17 ++ src/app/(customer)/(shop)/profile/page.tsx | 23 --- .../profile/profile-exit-account.tsx | 22 +++ src/components/profile/profile-menu-item.tsx | 38 +++++ src/components/profile/profile-menu.tsx | 40 +++++ .../profile/profile-page-head-line.tsx | 11 ++ .../profile/profile-person-detail-card.tsx | 22 +++ src/components/wallet/transaction-history.tsx | 38 +++++ src/components/wallet/transaction-item.tsx | 49 ++++++ src/components/wallet/wallet-blanc-card.tsx | 27 ++++ src/proxy.ts | 2 +- 13 files changed, 311 insertions(+), 160 deletions(-) create mode 100644 src/app/(customer)/(shop)/profile/(routes)/(landing)/page.tsx create mode 100644 src/app/(customer)/(shop)/profile/(routes)/wallet/page.tsx delete mode 100644 src/app/(customer)/(shop)/profile/page.tsx create mode 100644 src/components/profile/profile-exit-account.tsx create mode 100644 src/components/profile/profile-menu-item.tsx create mode 100644 src/components/profile/profile-menu.tsx create mode 100644 src/components/profile/profile-page-head-line.tsx create mode 100644 src/components/profile/profile-person-detail-card.tsx create mode 100644 src/components/wallet/transaction-history.tsx create mode 100644 src/components/wallet/transaction-item.tsx create mode 100644 src/components/wallet/wallet-blanc-card.tsx diff --git a/package-lock.json b/package-lock.json index 28b1c3a..003039d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -81,6 +81,7 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -290,28 +291,6 @@ "node": ">=6.9.0" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", - "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -683,9 +662,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -702,9 +678,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -721,9 +694,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -740,9 +710,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -759,9 +726,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -778,9 +742,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -797,9 +758,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -816,9 +774,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -835,9 +790,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -860,9 +812,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -885,9 +834,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -910,9 +856,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -935,9 +878,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -960,9 +900,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -985,9 +922,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1010,9 +944,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1280,9 +1211,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1299,9 +1227,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1318,9 +1243,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1337,9 +1259,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2282,9 +2201,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2302,9 +2218,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2322,9 +2235,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2342,9 +2252,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2506,6 +2413,7 @@ "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2516,6 +2424,7 @@ "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2565,6 +2474,7 @@ "integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.67.0", "@typescript-eslint/types": "8.67.0", @@ -2921,9 +2831,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2938,9 +2845,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2955,9 +2859,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2972,9 +2873,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2989,9 +2887,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3006,9 +2901,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3023,9 +2915,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3040,9 +2929,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3057,9 +2943,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3074,9 +2957,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3175,6 +3055,7 @@ "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3530,6 +3411,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.11.12", "caniuse-lite": "^1.0.30001809", @@ -4146,6 +4028,7 @@ "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4331,6 +4214,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5818,9 +5702,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5842,9 +5723,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5866,9 +5744,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5890,9 +5765,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6576,6 +6448,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6585,6 +6458,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -7361,6 +7235,7 @@ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7523,6 +7398,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7843,6 +7719,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/app/(customer)/(shop)/profile/(routes)/(landing)/page.tsx b/src/app/(customer)/(shop)/profile/(routes)/(landing)/page.tsx new file mode 100644 index 0000000..83b770d --- /dev/null +++ b/src/app/(customer)/(shop)/profile/(routes)/(landing)/page.tsx @@ -0,0 +1,33 @@ +import Link from "next/link"; +import { requireSession } from "@/lib/auth/session"; +import { LogoutButton } from "@/components/customer/logout-button"; +import ProfileListTileItem from "@/components/profile/profile-menu-item"; +import ProfileListMenu from "@/components/profile/profile-menu"; +import ProfilePageHeadLine from "@/components/profile/profile-page-head-line"; +import ProfilePersonDetailCard from "@/components/profile/profile-person-detail-card"; + +// Minimal placeholder — winofyfrontenddoc.md's bottom-nav references a "پروفایل" +// tab, but no profile screen is documented yet in either doc. +export default async function ProfilePage() { + // await requireSession("/profile"); + return
+ + + + +
+ // return ( + //
+ //

پروفایل

+ // + // سفارش‌های من + // + // + // آدرس‌های من + // + //
+ // + //
+ //
+ // ); +} diff --git a/src/app/(customer)/(shop)/profile/(routes)/wallet/page.tsx b/src/app/(customer)/(shop)/profile/(routes)/wallet/page.tsx new file mode 100644 index 0000000..9827f0b --- /dev/null +++ b/src/app/(customer)/(shop)/profile/(routes)/wallet/page.tsx @@ -0,0 +1,17 @@ +import ProfilePageHeadLine from '@/components/profile/profile-page-head-line' +import TransactionHistory from '@/components/wallet/transaction-history' +import WalletBlancCard from '@/components/wallet/wallet-blanc-card' +import React from 'react' + +const WalletPage = () => { + return ( +
+ + + + +
+ ) +} + +export default WalletPage \ No newline at end of file diff --git a/src/app/(customer)/(shop)/profile/page.tsx b/src/app/(customer)/(shop)/profile/page.tsx deleted file mode 100644 index 415b538..0000000 --- a/src/app/(customer)/(shop)/profile/page.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import Link from "next/link"; -import { requireSession } from "@/lib/auth/session"; -import { LogoutButton } from "@/components/customer/logout-button"; - -// Minimal placeholder — winofyfrontenddoc.md's bottom-nav references a "پروفایل" -// tab, but no profile screen is documented yet in either doc. -export default async function ProfilePage() { - await requireSession("/profile"); - return ( -
-

پروفایل

- - سفارش‌های من - - - آدرس‌های من - -
- -
-
- ); -} diff --git a/src/components/profile/profile-exit-account.tsx b/src/components/profile/profile-exit-account.tsx new file mode 100644 index 0000000..ac53b5b --- /dev/null +++ b/src/components/profile/profile-exit-account.tsx @@ -0,0 +1,22 @@ +'use client'; + +import { ChevronLeft, LogOutIcon } from "lucide-react" + +const ProfileExitAccount = () => { + return
{ alert('logout latter....') }} + > +
+
+ +
+ خروج +
+ +
+} + +export default ProfileExitAccount \ No newline at end of file diff --git a/src/components/profile/profile-menu-item.tsx b/src/components/profile/profile-menu-item.tsx new file mode 100644 index 0000000..885bbc4 --- /dev/null +++ b/src/components/profile/profile-menu-item.tsx @@ -0,0 +1,38 @@ +'use client'; + +import Link from 'next/link'; +import { ChevronLeft, User } from 'lucide-react'; + +interface ProfileMenuItemInterface { + href: string; + text: string; + icon?: React.ReactNode; + isLast?: boolean; +} + +const ProfileMenuItem = ({ + href, + text, + icon, + isLast = false, +}: ProfileMenuItemInterface) => { + return ( + +
+
+ {icon || } +
+ {text} +
+ + + ); +}; + +export default ProfileMenuItem; \ No newline at end of file diff --git a/src/components/profile/profile-menu.tsx b/src/components/profile/profile-menu.tsx new file mode 100644 index 0000000..bb670a2 --- /dev/null +++ b/src/components/profile/profile-menu.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { + ShoppingBag, + MapPin, + Wallet, + Bell, + Headset +} from 'lucide-react'; +import ProfileMenuItem from './profile-menu-item'; +import ProfileExitAccount from './profile-exit-account'; + +const menuItems = [ + { id: 1, href: '/profile/address', text: 'سعادت‌آباد', icon: }, + { id: 2, href: '/profile/orders', text: 'سفارش‌های من', icon: }, + { id: 3, href: '/profile/addresses', text: 'آدرس‌های من', icon: }, + { id: 4, href: '/profile/wallet', text: 'کیف پول', icon: }, + { id: 5, href: '/profile/notifications', text: 'اعلان‌ها', icon: }, + { id: 6, href: '/profile/support', text: 'پشتیبانی', icon: }, +]; + +const ProfileMenu = () => { + return ( +
+ {menuItems.map((item, index) => ( + + ))} +
+ +
+ ); +}; + +export default ProfileMenu; \ No newline at end of file diff --git a/src/components/profile/profile-page-head-line.tsx b/src/components/profile/profile-page-head-line.tsx new file mode 100644 index 0000000..cc4a40c --- /dev/null +++ b/src/components/profile/profile-page-head-line.tsx @@ -0,0 +1,11 @@ +import React from 'react' + +interface ProfilePageHeadLineInterface { + title: String; +} + +const ProfilePageHeadLine = ({ title }: ProfilePageHeadLineInterface) => { + return

{title}

+} + +export default ProfilePageHeadLine \ No newline at end of file diff --git a/src/components/profile/profile-person-detail-card.tsx b/src/components/profile/profile-person-detail-card.tsx new file mode 100644 index 0000000..ed99253 --- /dev/null +++ b/src/components/profile/profile-person-detail-card.tsx @@ -0,0 +1,22 @@ +'use client'; + +import React from 'react' + +const ProfilePersonDetailCard = () => { + return ( +
+
+
+ م.ر +
+
+

محمد رضایی

+

09121234567

+ +
+
+
+ ) +} + +export default ProfilePersonDetailCard \ No newline at end of file diff --git a/src/components/wallet/transaction-history.tsx b/src/components/wallet/transaction-history.tsx new file mode 100644 index 0000000..c168338 --- /dev/null +++ b/src/components/wallet/transaction-history.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import TransactionItem from './transaction-item'; + +const transactions = [ + { id: 1, amount: 250000, description: 'واریز از کارت', type: 'deposit' }, + { id: 2, amount: 45000, description: 'پرداخت فاکتور', type: 'withdraw' }, + { id: 3, amount: 120000, description: 'واریز نقدی', type: 'deposit' }, + { id: 4, amount: 780000, description: 'حقوق ماهیانه', type: 'deposit' }, + { id: 5, amount: 320000, description: 'خرید از فروشگاه', type: 'withdraw' }, + { id: 6, amount: 560000, description: 'واریز چک', type: 'deposit' }, + { id: 7, amount: 95000, description: 'شارژ اینترنت', type: 'withdraw' }, + { id: 8, amount: 1500000, description: 'پرداخت وام', type: 'withdraw' }, + { id: 9, amount: 430000, description: 'واریز از همراه', type: 'deposit' }, + { id: 10, amount: 210000, description: 'خرید آنلاین', type: 'withdraw' }, + { id: 11, amount: 670000, description: 'پاداش عملکرد', type: 'deposit' }, + { id: 12, amount: 185000, description: 'پرداخت قبوض', type: 'withdraw' }, + { id: 13, amount: 920000, description: 'فروش کالا', type: 'deposit' }, + { id: 14, amount: 380000, description: 'انتقال به دوست', type: 'withdraw' }, + { id: 15, amount: 110000, description: 'بازگشت وجه', type: 'deposit' }, +] as const; + +const TransactionHistory = () => { + return ( +
+ {transactions.map((item, index) => ( + + ))} +
+ ); +}; + +export default TransactionHistory; \ No newline at end of file diff --git a/src/components/wallet/transaction-item.tsx b/src/components/wallet/transaction-item.tsx new file mode 100644 index 0000000..c1d876b --- /dev/null +++ b/src/components/wallet/transaction-item.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { ArrowDownCircle, ArrowDownIcon, ArrowUp, ArrowUpCircle } from 'lucide-react'; + +interface TransactionItemProps { + amount: number; + description: string; + type: 'deposit' | 'withdraw'; + isLast?: boolean; +} + +const TransactionItem = ({ + amount, + description, + type, + isLast = false, +}: TransactionItemProps) => { + const isDeposit = type === 'deposit'; + const Icon = isDeposit ? ArrowDownIcon : ArrowUp; + const amountColor = isDeposit ? 'text-[#2E7D32]' : 'text-[#D32F2F]'; + const bgColor = isDeposit ? 'bg-[#2E7D3210]' : 'bg-[#D32F2F10]'; + const iconColor = isDeposit ? 'text-[#2E7D32]' : 'text-[#D32F2F]'; + + const formattedAmount = amount.toLocaleString('fa-IR'); + + return ( +
+
+
+ +
+ {description} +
+ +
+ + {isDeposit ? '+' : '-'} {formattedAmount} تومان + + ۱۴۰۳/۰۸/۲۴ +
+
+ ); +}; + +export default TransactionItem; \ No newline at end of file diff --git a/src/components/wallet/wallet-blanc-card.tsx b/src/components/wallet/wallet-blanc-card.tsx new file mode 100644 index 0000000..08decd0 --- /dev/null +++ b/src/components/wallet/wallet-blanc-card.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { LogOutIcon, PlusIcon } from 'lucide-react' +import React from 'react' + +const WalletBlancCard = () => { + return ( +
+

موجودی کیف پول

+

35000000 تومان

+ +
+ + +
+
+ ) +} + +export default WalletBlancCard \ No newline at end of file diff --git a/src/proxy.ts b/src/proxy.ts index c834035..728b2b1 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -12,7 +12,7 @@ const AUTH_REQUIRED_PREFIXES = [ "/order-groups", "/addresses", "/account", - "/profile", + // "/profile", "/notifications", ]; From 2cd5a9987d95faf655ead0b5665aad35c0db1dcd Mon Sep 17 00:00:00 2001 From: Mohammadali Date: Sat, 29 Aug 2026 08:09:32 +0330 Subject: [PATCH 02/12] add priceUtils -- formatter price --- src/components/wallet/transaction-item.tsx | 5 +-- src/components/wallet/wallet-blanc-card.tsx | 3 +- src/lib/utils/priceUtils.ts | 42 +++++++++++++++++++++ 3 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 src/lib/utils/priceUtils.ts diff --git a/src/components/wallet/transaction-item.tsx b/src/components/wallet/transaction-item.tsx index c1d876b..45c0f38 100644 --- a/src/components/wallet/transaction-item.tsx +++ b/src/components/wallet/transaction-item.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { ArrowDownCircle, ArrowDownIcon, ArrowUp, ArrowUpCircle } from 'lucide-react'; +import { formatPriceWithToman } from '@/lib/utils/priceUtils'; interface TransactionItemProps { amount: number; @@ -20,8 +21,6 @@ const TransactionItem = ({ const bgColor = isDeposit ? 'bg-[#2E7D3210]' : 'bg-[#D32F2F10]'; const iconColor = isDeposit ? 'text-[#2E7D32]' : 'text-[#D32F2F]'; - const formattedAmount = amount.toLocaleString('fa-IR'); - return (
- {isDeposit ? '+' : '-'} {formattedAmount} تومان + {isDeposit ? '+' : '-'} {formatPriceWithToman(amount)} ۱۴۰۳/۰۸/۲۴
diff --git a/src/components/wallet/wallet-blanc-card.tsx b/src/components/wallet/wallet-blanc-card.tsx index 08decd0..b87a119 100644 --- a/src/components/wallet/wallet-blanc-card.tsx +++ b/src/components/wallet/wallet-blanc-card.tsx @@ -1,5 +1,6 @@ 'use client'; +import { formatPriceWithToman } from '@/lib/utils/priceUtils'; import { LogOutIcon, PlusIcon } from 'lucide-react' import React from 'react' @@ -7,7 +8,7 @@ const WalletBlancCard = () => { return (

موجودی کیف پول

-

35000000 تومان

+

{formatPriceWithToman(35000000)}

-
-
-
- ) -} + const { data, error, isLoading } = useAccount(); -export default ProfilePersonDetailCard \ No newline at end of file + if (isLoading) { + return ( +
+

در حال بارگذاری...

+
+ ); + } + + if (error) { + return ( +
+

{error}

+
+ ); + } + + if (!data) { + return ( +
+

اطلاعات پروفایل یافت نشد

+
+ ); + } + + const displayName = getDisplayNameHelper(data); + const initials = getInitialsHelper(data); + + return ( +
+
+
+ {data.avatar ? ( + {displayName} + ) : ( + {initials} + )} +
+
+

{displayName}

+ + {data.phone_number && ( +

+ {data.phone_number} +

+ )} + + +
+
+
+ ); +}; + +export default ProfilePersonDetailCard; \ No newline at end of file diff --git a/src/components/wallet/transaction-history.tsx b/src/components/wallet/transaction-history.tsx index c168338..38d9721 100644 --- a/src/components/wallet/transaction-history.tsx +++ b/src/components/wallet/transaction-history.tsx @@ -27,7 +27,7 @@ const TransactionHistory = () => { key={item.id} amount={item.amount} description={item.description} - type={item.type} // حالا نوعش دقیقاً 'deposit' | 'withdraw' هست + type={item.type} isLast={index === transactions.length - 1} /> ))} diff --git a/src/components/wallet/transaction-item.tsx b/src/components/wallet/transaction-item.tsx index 45c0f38..6bf2c7c 100644 --- a/src/components/wallet/transaction-item.tsx +++ b/src/components/wallet/transaction-item.tsx @@ -1,48 +1,49 @@ import React from 'react'; -import { ArrowDownCircle, ArrowDownIcon, ArrowUp, ArrowUpCircle } from 'lucide-react'; +import { ArrowDownIcon, ArrowUp } from 'lucide-react'; import { formatPriceWithToman } from '@/lib/utils/priceUtils'; interface TransactionItemProps { - amount: number; - description: string; - type: 'deposit' | 'withdraw'; - isLast?: boolean; + amount: number; + description: string; + type: 'deposit' | 'withdraw'; + isLast?: boolean; } const TransactionItem = ({ - amount, - description, - type, - isLast = false, + amount, + description, + type, + isLast = false, }: TransactionItemProps) => { - const isDeposit = type === 'deposit'; - const Icon = isDeposit ? ArrowDownIcon : ArrowUp; - const amountColor = isDeposit ? 'text-[#2E7D32]' : 'text-[#D32F2F]'; - const bgColor = isDeposit ? 'bg-[#2E7D3210]' : 'bg-[#D32F2F10]'; - const iconColor = isDeposit ? 'text-[#2E7D32]' : 'text-[#D32F2F]'; + const isDeposit = type === 'deposit'; + const Icon = isDeposit ? ArrowDownIcon : ArrowUp; + + const amountColor = isDeposit ? 'text-success' : 'text-error'; + const bgColor = isDeposit ? 'bg-success/10' : 'bg-error/10'; + const iconColor = isDeposit ? 'text-success' : 'text-error'; - return ( -
-
-
- -
- {description} -
- -
- - {isDeposit ? '+' : '-'} {formatPriceWithToman(amount)} - - ۱۴۰۳/۰۸/۲۴ -
+ > +
+
+
- ); + {description} +
+ +
+ + {isDeposit ? '+' : '-'} {formatPriceWithToman(amount)} + + ۱۴۰۳/۰۸/۲۴ +
+
+ ); }; export default TransactionItem; \ No newline at end of file diff --git a/src/components/wallet/wallet-blanc-card.tsx b/src/components/wallet/wallet-blanc-card.tsx index b87a119..26b073f 100644 --- a/src/components/wallet/wallet-blanc-card.tsx +++ b/src/components/wallet/wallet-blanc-card.tsx @@ -1,28 +1,52 @@ 'use client'; import { formatPriceWithToman } from '@/lib/utils/priceUtils'; -import { LogOutIcon, PlusIcon } from 'lucide-react' -import React from 'react' +import { LogOutIcon, PlusIcon } from 'lucide-react'; const WalletBlancCard = () => { return ( -
-

موجودی کیف پول

-

{formatPriceWithToman(35000000)}

+
+

+ موجودی کیف پول +

+

+ {formatPriceWithToman(35000000)} +

- -
- ) -} + ); +}; -export default WalletBlancCard \ No newline at end of file +export default WalletBlancCard; \ No newline at end of file diff --git a/src/lib/api/account.ts b/src/lib/api/account.ts new file mode 100644 index 0000000..0ec2102 --- /dev/null +++ b/src/lib/api/account.ts @@ -0,0 +1,8 @@ +"use server"; + +import { gooyalFetch } from "./gooyal-accounts"; +import type { Account } from "@/types/api"; + +export async function getAccount(): Promise { + return gooyalFetch("/users/api/account/"); +} \ No newline at end of file diff --git a/src/lib/api/gooyal-accounts.ts b/src/lib/api/gooyal-accounts.ts new file mode 100644 index 0000000..92a0e16 --- /dev/null +++ b/src/lib/api/gooyal-accounts.ts @@ -0,0 +1,65 @@ +import "server-only"; +import { getValidSession } from "@/lib/auth/session"; +import { ApiError, type WinofyErrorDetails } from "./errors"; +import { logWinofyRequest, logWinofyResponse } from "./dev-logger"; + +const BASE_URL = process.env.GOOYAL_ACCOUNTS_BASE_URL!; + +interface GooyalFetchOptions extends Omit { + auth?: boolean; + params?: Record; + body?: unknown; +} + +export async function gooyalFetch(path: string, options: GooyalFetchOptions = {}): Promise { + const { auth = true, params, headers, body, ...rest } = options; + const method = rest.method ?? "GET"; + + const url = new URL(BASE_URL.replace(/\/$/, "") + path); + if (params) { + for (const [key, value] of Object.entries(params)) { + if (value !== undefined) url.searchParams.set(key, String(value)); + } + } + + const finalHeaders = new Headers(headers); + const hasBody = body !== undefined; + if (hasBody && !finalHeaders.has("Content-Type")) { + finalHeaders.set("Content-Type", "application/json"); + } + + if (auth) { + const session = await getValidSession(); + if (session) { + finalHeaders.set("Authorization", `Bearer ${session.accessToken}`); + } + } + + logWinofyRequest?.(method, url.toString(), body); + + const res = await fetch(url.toString(), { + ...rest, + headers: finalHeaders, + body: hasBody ? JSON.stringify(body) : undefined, + cache: "no-store", + }); + + if (res.status === 204) { + logWinofyResponse?.(method, url.toString(), res.status, null); + return undefined as T; + } + + const data = await res.json().catch(() => null); + logWinofyResponse?.(method, url.toString(), res.status, data); + + if (!res.ok) { + const envelope = data as { status_message?: string; details?: WinofyErrorDetails } | null; + throw new ApiError( + res.status, + envelope?.status_message ?? res.statusText, + envelope?.details ?? null, + ); + } + + return data as T; +} \ No newline at end of file diff --git a/src/lib/auth/gooyal-client.ts b/src/lib/auth/gooyal-client.ts index 132c4a2..6b5e404 100644 --- a/src/lib/auth/gooyal-client.ts +++ b/src/lib/auth/gooyal-client.ts @@ -95,4 +95,4 @@ export async function exchangeOtpForToken(phoneNumber: string, otp: string) { throw new GooyalClientError(res.status, data.error ?? "token_exchange_failed", data.error_description); } return data as GooyalTokenResponse; -} +} \ No newline at end of file diff --git a/src/lib/providers/account-provider.tsx b/src/lib/providers/account-provider.tsx new file mode 100644 index 0000000..fe4ba14 --- /dev/null +++ b/src/lib/providers/account-provider.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { createContext, useContext, type ReactNode } from "react"; +import type { Account } from "@/types/api"; + +type AccountContextValue = { + data: Account | null; + error: string | null; + isLoading: boolean; +}; + +const AccountContext = createContext(undefined); + +export function AccountProvider({ + children, + account, + error = null, +}: { + children: ReactNode; + account: Account | null; + error?: string | null; +}) { + return ( + + {children} + + ); +} + +export function useAccount() { + const ctx = useContext(AccountContext); + if (ctx === undefined) { + throw new Error("useAccount must be used within AccountProvider"); + } + return ctx; +} \ No newline at end of file diff --git a/src/lib/utils/account-utils.ts b/src/lib/utils/account-utils.ts new file mode 100644 index 0000000..fbd53e9 --- /dev/null +++ b/src/lib/utils/account-utils.ts @@ -0,0 +1,49 @@ +'use client'; + +import { useAccount } from '@/lib/providers/account-provider'; +import React from 'react'; + +export function getInitialsHelper(account: { + name?: string | null; + first_name?: string | null; + last_name?: string | null; + username?: string | null; +}): string { + if (account.name?.trim()) { + const parts = account.name.trim().split(/\s+/); + if (parts.length >= 2) { + return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); + } + return parts[0].slice(0, 2).toUpperCase(); + } + + if (account.first_name || account.last_name) { + const first = account.first_name?.[0] ?? ''; + const last = account.last_name?.[0] ?? ''; + return (first + last).toUpperCase() || '؟'; + } + + if (account.username) { + return account.username.slice(0, 2).toUpperCase(); + } + + return '؟'; +} + +export function getDisplayNameHelper(account: { + name?: string | null; + first_name?: string | null; + last_name?: string | null; + username?: string | null; +}): string { + if (account.name?.trim()) return account.name.trim(); + + const fullName = [account.first_name, account.last_name] + .filter(Boolean) + .join(' ') + .trim(); + + if (fullName) return fullName; + + return account.username || 'کاربر'; +} \ No newline at end of file diff --git a/src/types/api.ts b/src/types/api.ts index c0c86e3..2945024 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -310,3 +310,42 @@ export interface SellerAnalytics { chart: SalesChartPoint[]; top_products: TopProduct[]; } + + +// ---- Account / Profile ---------------------------------------------------- + +export type Gender = "m" | "f" | "other" | null; +export type EducationDegree = + | "primary" + | "secondary" + | "diploma" + | "bachelor" + | "master" + | "phd" + | string + | null; + +export interface Account { + uuid: string; + username: string; + email: string | null; + phone_number: string | null; + first_name: string | null; + last_name: string | null; + name: string | null; + avatar: string | null; + gender: Gender; + education_degree: EducationDegree; + education_field: string | null; + birthdate: string | null; // "YYYY-MM-DD" + iban: string | null; + iban_verified: boolean; + province: number | null; + province_name: string | null; + city: number | null; + city_name: string | null; + postal_code: string | null; + address: string | null; + date_joined: string; + last_update: string; +} \ No newline at end of file From cc928871cc3be32a8f5ac7afb38c013a8cb47a63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afra=20=E2=80=8C?= Date: Mon, 31 Aug 2026 16:03:11 +0330 Subject: [PATCH 06/12] perf: logger + store page modification --- src/app/(customer)/(shop)/cart/page.tsx | 6 +- .../(customer)/(shop)/stores/[uuid]/page.tsx | 53 ++++++-- src/components/auth/role-card.tsx | 2 +- .../customer/add-to-cart-button.tsx | 54 -------- src/components/customer/cart-item-row.tsx | 91 +++++++------ src/components/customer/clear-cart-button.tsx | 48 +++++++ src/components/store/product-grid-card.tsx | 122 +++++++++++------- src/components/store/store-info-card.tsx | 51 +++++--- src/components/ui/button.tsx | 10 +- src/components/ui/hero-back-button.tsx | 4 +- src/lib/api/dev-logger.ts | 14 +- src/lib/api/winofy.ts | 8 +- src/lib/auth/gooyal.ts | 14 +- 13 files changed, 289 insertions(+), 188 deletions(-) delete mode 100644 src/components/customer/add-to-cart-button.tsx create mode 100644 src/components/customer/clear-cart-button.tsx diff --git a/src/app/(customer)/(shop)/cart/page.tsx b/src/app/(customer)/(shop)/cart/page.tsx index 31bb900..243db0c 100644 --- a/src/app/(customer)/(shop)/cart/page.tsx +++ b/src/app/(customer)/(shop)/cart/page.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { getCart } from "@/lib/api/cart"; import { requireSession } from "@/lib/auth/session"; import { CartItemRow } from "@/components/customer/cart-item-row"; +import { ClearCartButton } from "@/components/customer/clear-cart-button"; import { Button } from "@/components/ui/button"; export default async function CartPage() { @@ -22,7 +23,10 @@ export default async function CartPage() { return (
-

سبد خرید

+
+

سبد خرید

+ +
{cart.groups.map((group) => (
diff --git a/src/app/(customer)/(shop)/stores/[uuid]/page.tsx b/src/app/(customer)/(shop)/stores/[uuid]/page.tsx index 9102f9a..ed111ff 100644 --- a/src/app/(customer)/(shop)/stores/[uuid]/page.tsx +++ b/src/app/(customer)/(shop)/stores/[uuid]/page.tsx @@ -25,45 +25,70 @@ export default async function StoreDetailPage({ }); if (!store) notFound(); - const [products, session] = await Promise.all([getProducts({ store: uuid, search, limit: 100 }), getValidSession()]); + const [products, session] = await Promise.all([ + getProducts({ store: uuid, search, limit: 100 }), + getValidSession(), + ]); const cart = session ? await getCart() : null; const storeGroup = cart?.groups.find((g) => g.store.uuid === uuid) ?? null; const cartItemByProduct = new Map(); - storeGroup?.items.forEach((item) => cartItemByProduct.set(item.product.uuid, item)); + storeGroup?.items.forEach((item) => + cartItemByProduct.set(item.product.uuid, item), + ); return (
-
+
{store.cover_image_url && ( // eslint-disable-next-line @next/next/no-img-element - + )}
+
+ +
- - -
-

توضیحات

-

{store.description}

+
+

+ توضیحات +

+

+ {store.description} +

-
+
-
+
{products.map((product) => ( - + ))} {products.length === 0 && ( -

محصولی یافت نشد.

+

+ محصولی یافت نشد. +

)}
- {storeGroup && } + {storeGroup && ( + + )}
); } diff --git a/src/components/auth/role-card.tsx b/src/components/auth/role-card.tsx index f247116..5cdddb1 100644 --- a/src/components/auth/role-card.tsx +++ b/src/components/auth/role-card.tsx @@ -31,7 +31,7 @@ export function RoleCard({ className={cn( "flex h-12 w-full items-center justify-center rounded-full text-body-large font-semibold transition-colors", ctaVariant === "primary" - ? "bg-primary-action text-white hover:bg-primary-action/90" + ? "border-0 bg-primary-action text-white hover:bg-primary-action/90" : "border border-primary-action bg-white text-primary-action hover:bg-primary-action/5", )} > diff --git a/src/components/customer/add-to-cart-button.tsx b/src/components/customer/add-to-cart-button.tsx deleted file mode 100644 index f860672..0000000 --- a/src/components/customer/add-to-cart-button.tsx +++ /dev/null @@ -1,54 +0,0 @@ -"use client"; - -import { useState, useTransition } from "react"; -import { useRouter, usePathname } from "next/navigation"; -import { addToCart } from "@/lib/api/cart"; -import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils/cn"; - -export function AddToCartButton({ - productUuid, - quantity = 1, - disabled, - outOfStock, - className, -}: { - productUuid: string; - quantity?: number; - disabled?: boolean; - outOfStock?: boolean; - className?: string; -}) { - const router = useRouter(); - const pathname = usePathname(); - const [isPending, startTransition] = useTransition(); - const [feedback, setFeedback] = useState<"added" | "error" | null>(null); - - function handleClick() { - startTransition(async () => { - const result = await addToCart(productUuid, quantity); - if (result.ok) { - setFeedback("added"); - router.refresh(); - setTimeout(() => setFeedback(null), 1500); - } else if (result.status === 401) { - router.push(`/login?next=${encodeURIComponent(pathname)}`); - } else { - setFeedback("error"); - } - }); - } - - return ( - - ); -} diff --git a/src/components/customer/cart-item-row.tsx b/src/components/customer/cart-item-row.tsx index 923824b..0a92ad9 100644 --- a/src/components/customer/cart-item-row.tsx +++ b/src/components/customer/cart-item-row.tsx @@ -1,59 +1,74 @@ "use client"; -import { useTransition } from "react"; -import { useRouter } from "next/navigation"; +import { useEffect, useState, useTransition } from "react"; +import { useRouter, usePathname } from "next/navigation"; import { updateCartItemQuantity, removeCartItem } from "@/lib/api/cart"; import type { CartItem } from "@/types/api"; export function CartItemRow({ item }: { item: CartItem }) { const router = useRouter(); + const pathname = usePathname(); const [isPending, startTransition] = useTransition(); + const [error, setError] = useState(null); + + useEffect(() => { + if (!error) return; + const timeout = setTimeout(() => setError(null), 3000); + return () => clearTimeout(timeout); + }, [error]); function changeQuantity(nextQuantity: number) { startTransition(async () => { - if (nextQuantity <= 0) { - await removeCartItem(item.uuid); + const result = + nextQuantity <= 0 ? await removeCartItem(item.uuid) : await updateCartItemQuantity(item.uuid, nextQuantity); + + if (result.ok) { + router.refresh(); + } else if (result.status === 401) { + router.push(`/login?next=${encodeURIComponent(pathname)}`); } else { - await updateCartItemQuantity(item.uuid, nextQuantity); + setError(result.message); } - router.refresh(); }); } return ( -
-
- {item.product.image_url && ( - // eslint-disable-next-line @next/next/no-img-element - - )} +
+
+
+ {item.product.image_url && ( + // eslint-disable-next-line @next/next/no-img-element + + )} +
+
+ {item.product.name} + {item.product.price.toLocaleString("fa-IR")} تومان +
+
+ + {item.quantity.toLocaleString("fa-IR")} + +
+ {item.line_total.toLocaleString("fa-IR")}
-
- {item.product.name} - {item.product.price.toLocaleString("fa-IR")} تومان -
-
- - {item.quantity} - -
- {item.line_total.toLocaleString("fa-IR")} + {error &&

{error}

}
); } diff --git a/src/components/customer/clear-cart-button.tsx b/src/components/customer/clear-cart-button.tsx new file mode 100644 index 0000000..708e0e7 --- /dev/null +++ b/src/components/customer/clear-cart-button.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { useEffect, useState, useTransition } from "react"; +import { useRouter, usePathname } from "next/navigation"; +import { Trash2 } from "lucide-react"; +import { clearCart } from "@/lib/api/cart"; + +export function ClearCartButton() { + const router = useRouter(); + const pathname = usePathname(); + const [isPending, startTransition] = useTransition(); + const [error, setError] = useState(null); + + useEffect(() => { + if (!error) return; + const timeout = setTimeout(() => setError(null), 3000); + return () => clearTimeout(timeout); + }, [error]); + + function handleClear() { + if (!window.confirm("سبد خرید شما به‌طور کامل خالی می‌شود. ادامه می‌دهید؟")) return; + startTransition(async () => { + const result = await clearCart(); + if (result.ok) { + router.refresh(); + } else if (result.status === 401) { + router.push(`/login?next=${encodeURIComponent(pathname)}`); + } else { + setError(result.message); + } + }); + } + + return ( +
+ + {error &&

{error}

} +
+ ); +} diff --git a/src/components/store/product-grid-card.tsx b/src/components/store/product-grid-card.tsx index defcdf8..e840bac 100644 --- a/src/components/store/product-grid-card.tsx +++ b/src/components/store/product-grid-card.tsx @@ -4,11 +4,20 @@ import { useEffect, useState, useTransition } from "react"; import Link from "next/link"; import { useRouter, usePathname } from "next/navigation"; import { Minus, Plus } from "lucide-react"; -import { addToCart, updateCartItemQuantity, removeCartItem } from "@/lib/api/cart"; +import { + addToCart, + updateCartItemQuantity, + removeCartItem, +} from "@/lib/api/cart"; import type { CartItem, ProductListItem } from "@/types/api"; -/** designspec.md §2.5 — product-card, 157×254, with the two-state add/stepper control. */ -export function ProductGridCard({ product, cartItem }: { product: ProductListItem; cartItem: CartItem | null }) { +export function ProductGridCard({ + product, + cartItem, +}: { + product: ProductListItem; + cartItem: CartItem | null; +}) { const router = useRouter(); const pathname = usePathname(); const [isPending, startTransition] = useTransition(); @@ -43,12 +52,13 @@ export function ProductGridCard({ product, cartItem }: { product: ProductListIte startTransition(async () => { if (nextQuantity <= 0) { const result = await removeCartItem(localItem.uuid); - // Only clear locally once the backend actually confirms removal — otherwise - // the card would show "add" again while the item still exists server-side. if (result.ok) setLocalItem(null); else setError(result.message); } else { - const result = await updateCartItemQuantity(localItem.uuid, nextQuantity); + const result = await updateCartItemQuantity( + localItem.uuid, + nextQuantity, + ); if (result.ok) setLocalItem(result.data); else setError(result.message); } @@ -57,67 +67,83 @@ export function ProductGridCard({ product, cartItem }: { product: ProductListIte } return ( -
+
{error && (
{error}
)} -
+
{product.image_url && ( // eslint-disable-next-line @next/next/no-img-element - {product.name} + {product.name} )} -
+
-
-
- -

{product.name}

- -

- {product.price.toLocaleString("fa-IR")} تومان -

-
+
+ +

+ {product.name} +

+ +

+ {product.price.toLocaleString("fa-IR")} تومان +

+ {disabled && ( + + ناموجود + + )} +
- {disabled ? ( - ناموجود - ) : quantity === 0 ? ( - - ) : ( -
+ {!disabled && ( +
+
- {quantity.toLocaleString("fa-IR")} - + + {quantity > 0 && ( + <> + + {quantity.toLocaleString("fa-IR")} + + + + + )}
- )} -
+
+ )}
); } diff --git a/src/components/store/store-info-card.tsx b/src/components/store/store-info-card.tsx index 2f21db1..c07dc4c 100644 --- a/src/components/store/store-info-card.tsx +++ b/src/components/store/store-info-card.tsx @@ -6,7 +6,9 @@ import type { StoreDetail } from "@/types/api"; function todaysWorkingHours(store: StoreDetail) { const jsDay = new Date().getDay(); const iranianWeekday = (jsDay + 1) % 7; - return store.working_hours.find((wh) => wh.weekday === iranianWeekday) ?? null; + return ( + store.working_hours.find((wh) => wh.weekday === iranianWeekday) ?? null + ); } function formatTime(value: string | null) { @@ -20,35 +22,52 @@ export function StoreInfoCard({ store }: { store: StoreDetail }) { const mapsUrl = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(`${store.name} ${store.address}`)}`; return ( -
-

{store.name}

-

{store.category.name}

+
+

+ {store.name} +

+

+ {store.category.name} +

- {today && ( - - {today.is_closed ? "تعطیل" : `${formatTime(today.opens_at)} تا ${formatTime(today.closes_at)}`} - - )} - | - + {store.is_open ? "فعال" : "بسته"} + | + {today && ( + + {today.is_closed + ? "تعطیل" + : `${formatTime(today.opens_at)} تا ${formatTime(today.closes_at)}`} + + )}
-
+ @@ -56,7 +75,7 @@ export function StoreInfoCard({ store }: { store: StoreDetail }) { {store.phone_number && ( diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 232d283..500f777 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -8,10 +8,14 @@ const buttonVariants = cva( { variants: { variant: { - primary: "bg-primary text-primary-foreground hover:bg-primary/90", - secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + // Tailwind's preflight doesn't zero out - {toPersianDigits(quantity)} + {toPersianDigits(quantity)} +
- - {product.is_out_of_stock ? "ناموجود" : "موجود ●"} -
@@ -73,35 +81,35 @@ export function ProductPurchasePanel({ product, children }: { product: ProductDe {error &&

{error}

} {showToast && ( -
- +
+ محصول به سبد خرید اضافه شد - +
)} -
+
-
جمع کل {(product.price * quantity).toLocaleString("fa-IR")} تومان
+
-
+
); diff --git a/src/components/ui/hero-back-button.tsx b/src/components/ui/hero-back-button.tsx index f2e5dbd..98ab8d7 100644 --- a/src/components/ui/hero-back-button.tsx +++ b/src/components/ui/hero-back-button.tsx @@ -7,9 +7,9 @@ export function HeroBackButton({ href }: { href: string }) { - + ); } From 64e321dc3d7ab3aa9020b9e60bdf2076bc82d634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afra=20=E2=80=8C?= Date: Wed, 2 Sep 2026 00:38:22 +0330 Subject: [PATCH 08/12] perf: store and product pages' bottom bar modified --- .../product/product-purchase-panel.tsx | 2 +- src/components/store/sticky-cart-bar.tsx | 33 +++++++++---------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/components/product/product-purchase-panel.tsx b/src/components/product/product-purchase-panel.tsx index ca5a299..d7f431e 100644 --- a/src/components/product/product-purchase-panel.tsx +++ b/src/components/product/product-purchase-panel.tsx @@ -94,7 +94,7 @@ export function ProductPurchasePanel({ product, children }: { product: ProductDe
-
+
جمع کل {(product.price * quantity).toLocaleString("fa-IR")} تومان diff --git a/src/components/store/sticky-cart-bar.tsx b/src/components/store/sticky-cart-bar.tsx index c0744a8..523f79d 100644 --- a/src/components/store/sticky-cart-bar.tsx +++ b/src/components/store/sticky-cart-bar.tsx @@ -1,32 +1,31 @@ import Link from "next/link"; -import { ShoppingBag } from "lucide-react"; /** designspec.md §2.6 — only rendered when this store's cart group actually has items. */ export function StickyCartBar({ itemCount, total }: { itemCount: number; total: number }) { return ( -
+
+ {/* DOM order intentionally mirrors the screenshot's left→right layout, not reading + order — under this app's inherited dir="rtl", the first child renders at the + right edge and the last at the left. */}
- - - - - {itemCount.toLocaleString("fa-IR")} - - - مشاهده سبد - - -
+
جمع کل {total.toLocaleString("fa-IR")} تومان
+ + + مشاهده سبد + + {itemCount.toLocaleString("fa-IR")} + +
-
+
); } From adf31dc999949b40b7d4d1865508f8f8193c2ab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afra=20=E2=80=8C?= Date: Wed, 2 Sep 2026 01:30:30 +0330 Subject: [PATCH 09/12] perf: cart page modified --- src/app/(customer)/(shop)/cart/page.tsx | 124 ++++++++++++++-------- src/components/customer/cart-item-row.tsx | 44 +++++--- 2 files changed, 110 insertions(+), 58 deletions(-) diff --git a/src/app/(customer)/(shop)/cart/page.tsx b/src/app/(customer)/(shop)/cart/page.tsx index 243db0c..e5e3def 100644 --- a/src/app/(customer)/(shop)/cart/page.tsx +++ b/src/app/(customer)/(shop)/cart/page.tsx @@ -1,14 +1,13 @@ import Link from "next/link"; +import { ArrowRight, ShoppingBasket } from "lucide-react"; import { getCart } from "@/lib/api/cart"; import { requireSession } from "@/lib/auth/session"; import { CartItemRow } from "@/components/customer/cart-item-row"; -import { ClearCartButton } from "@/components/customer/clear-cart-button"; import { Button } from "@/components/ui/button"; export default async function CartPage() { await requireSession("/cart"); const cart = await getCart(); - const hasBlockedGroup = cart.groups.some((g) => !g.meets_minimum_order); if (cart.groups.length === 0) { return ( @@ -21,58 +20,93 @@ export default async function CartPage() { ); } + const totalItemLines = cart.groups.reduce( + (sum, g) => sum + g.items.length, + 0, + ); + return ( -
-
-

سبد خرید

- +
+ {/* DOM order intentionally mirrors the screenshot's left→right layout, not reading + order — under this app's inherited dir="rtl", the first child renders at the + right edge and the last at the left. */} +
+ + + +

+ سبد خرید +

- {cart.groups.map((group) => ( -
- - {group.store.name} - +
+ {cart.groups.map((group) => ( +
+ + {group.items.length.toLocaleString("fa-IR")} کالا + -
- {group.items.map((item) => ( - - ))} -
+ + + + {group.store.name} + + -
-
- جمع کالاها - {group.items_subtotal.toLocaleString("fa-IR")} تومان +
+ {group.items.map((item) => ( + + ))}
-
- هزینه ارسال - {group.delivery_fee.toLocaleString("fa-IR")} تومان -
-
- مجموع - {group.total.toLocaleString("fa-IR")} تومان -
-
- {!group.meets_minimum_order && ( -

- حداقل مبلغ سفارش برای این فروشگاه رعایت نشده است. -

- )} -
- ))} +
+ + جمع فروشگاه: {group.total.toLocaleString("fa-IR")} تومان + +
-
-
- مبلغ قابل پرداخت - {cart.grand_total.toLocaleString("fa-IR")} تومان + {!group.meets_minimum_order && ( +

+ حداقل مبلغ سفارش برای این فروشگاه رعایت نشده است. +

+ )} + + + + +
+ ))} +
+ +
+
+ + جمع کل سبد ({cart.groups.length.toLocaleString("fa-IR")} فروشگاه،{" "} + {totalItemLines.toLocaleString("fa-IR")} کالا) + +

+ هر فروشگاه جداگانه پرداخت و ارسال می‌شود +

- - - + + {cart.grand_total.toLocaleString("fa-IR")} تومان +
); diff --git a/src/components/customer/cart-item-row.tsx b/src/components/customer/cart-item-row.tsx index 0a92ad9..df582a2 100644 --- a/src/components/customer/cart-item-row.tsx +++ b/src/components/customer/cart-item-row.tsx @@ -2,6 +2,7 @@ import { useEffect, useState, useTransition } from "react"; import { useRouter, usePathname } from "next/navigation"; +import { Minus, Plus, Trash2 } from "lucide-react"; import { updateCartItemQuantity, removeCartItem } from "@/lib/api/cart"; import type { CartItem } from "@/types/api"; @@ -33,41 +34,58 @@ export function CartItemRow({ item }: { item: CartItem }) { } return ( -
-
-
+
+ {/* DOM order intentionally mirrors the screenshot's left→right layout, not reading + order — under this app's inherited dir="rtl", the first child renders at the + right edge and the last at the left. */} +
+
{item.product.image_url && ( // eslint-disable-next-line @next/next/no-img-element )}
-
- {item.product.name} - {item.product.price.toLocaleString("fa-IR")} تومان +
+ {item.product.name} + + {item.product.price.toLocaleString("fa-IR")} تومان +
-
+
+ +
+
- {item.quantity.toLocaleString("fa-IR")} + + {item.quantity.toLocaleString("fa-IR")} +
- {item.line_total.toLocaleString("fa-IR")} +
+ {error &&

{error}

}
); From f94ee531a3f5c263baa2b8ae13c1dbd87d238518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afra=20=E2=80=8C?= Date: Wed, 2 Sep 2026 01:55:25 +0330 Subject: [PATCH 10/12] feat: checout page implemented --- src/app/(customer)/(shop)/cart/page.tsx | 39 +------ .../(shop)/checkout/[store]/page.tsx | 31 +++++ .../customer/checkout-fulfillment.tsx | 108 ++++++++++++++++++ src/components/customer/store-cart-card.tsx | 38 ++++++ 4 files changed, 182 insertions(+), 34 deletions(-) create mode 100644 src/app/(customer)/(shop)/checkout/[store]/page.tsx create mode 100644 src/components/customer/checkout-fulfillment.tsx create mode 100644 src/components/customer/store-cart-card.tsx diff --git a/src/app/(customer)/(shop)/cart/page.tsx b/src/app/(customer)/(shop)/cart/page.tsx index e5e3def..56ee906 100644 --- a/src/app/(customer)/(shop)/cart/page.tsx +++ b/src/app/(customer)/(shop)/cart/page.tsx @@ -1,8 +1,8 @@ import Link from "next/link"; -import { ArrowRight, ShoppingBasket } from "lucide-react"; +import { ArrowRight } from "lucide-react"; import { getCart } from "@/lib/api/cart"; import { requireSession } from "@/lib/auth/session"; -import { CartItemRow } from "@/components/customer/cart-item-row"; +import { StoreCartCard } from "@/components/customer/store-cart-card"; import { Button } from "@/components/ui/button"; export default async function CartPage() { @@ -41,36 +41,7 @@ export default async function CartPage() {
{cart.groups.map((group) => ( -
- - {group.items.length.toLocaleString("fa-IR")} کالا - - - - - - {group.store.name} - - - -
- {group.items.map((item) => ( - - ))} -
- -
- - جمع فروشگاه: {group.total.toLocaleString("fa-IR")} تومان - -
- + {!group.meets_minimum_order && (

حداقل مبلغ سفارش برای این فروشگاه رعایت نشده است. @@ -78,7 +49,7 @@ export default async function CartPage() { )} @@ -90,7 +61,7 @@ export default async function CartPage() { تکمیل سفارش -

+ ))}
diff --git a/src/app/(customer)/(shop)/checkout/[store]/page.tsx b/src/app/(customer)/(shop)/checkout/[store]/page.tsx new file mode 100644 index 0000000..b295587 --- /dev/null +++ b/src/app/(customer)/(shop)/checkout/[store]/page.tsx @@ -0,0 +1,31 @@ +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { ArrowRight } from "lucide-react"; +import { getCart } from "@/lib/api/cart"; +import { requireSession } from "@/lib/auth/session"; +import { StoreCartCard } from "@/components/customer/store-cart-card"; +import { CheckoutFulfillment } from "@/components/customer/checkout-fulfillment"; + +export default async function StoreCheckoutPage({ params }: PageProps<"/checkout/[store]">) { + const { store } = await params; + await requireSession(`/checkout/${store}`); + const cart = await getCart(); + const group = cart.groups.find((g) => g.store.uuid === store); + if (!group) notFound(); + + return ( +
+ {/* Same header as /cart, same DOM-order note: first child renders right, last left. */} +
+ + + +

سبد خرید

+
+ + + + +
+ ); +} diff --git a/src/components/customer/checkout-fulfillment.tsx b/src/components/customer/checkout-fulfillment.tsx new file mode 100644 index 0000000..7ca7b10 --- /dev/null +++ b/src/components/customer/checkout-fulfillment.tsx @@ -0,0 +1,108 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import type { CartGroup } from "@/types/api"; + +/** + * "دریافت حضوری" vs "ارسال توسط فروشگاه" has no backing field in the Checkout API — + * the OpenAPI spec's Checkout schema is address_uuid/delivery_type/scheduled_at/ + * payment_method/notes only, with delivery_type meaning express-vs-scheduled timing, + * not pickup-vs-delivery fulfillment. So this toggle only affects what's *displayed* + * here; the actual order submission (next screen) still uses the real delivery fee. + */ +export function CheckoutFulfillment({ group }: { group: CartGroup }) { + const [pickup, setPickup] = useState(false); + const deliveryFee = pickup ? 0 : group.delivery_fee; + const payable = group.items_subtotal + deliveryFee; + + return ( +
+
+

نحوه دریافت

+ + + + +
+ +
+
+ جمع کالاها + {group.items_subtotal.toLocaleString("fa-IR")} تومان +
+
+ هزینه ارسال + {deliveryFee.toLocaleString("fa-IR")} تومان +
+
+ مبلغ قابل پرداخت + + {payable.toLocaleString("fa-IR")} تومان + +
+
+ +
+ + + +
+
+ ); +} diff --git a/src/components/customer/store-cart-card.tsx b/src/components/customer/store-cart-card.tsx new file mode 100644 index 0000000..9500a32 --- /dev/null +++ b/src/components/customer/store-cart-card.tsx @@ -0,0 +1,38 @@ +import Link from "next/link"; +import { ShoppingBasket } from "lucide-react"; +import { CartItemRow } from "@/components/customer/cart-item-row"; +import type { CartGroup } from "@/types/api"; +import type { ReactNode } from "react"; + +/** Shared between /cart and /checkout/[store] — the pixel-perfect per-store card. */ +export function StoreCartCard({ group, children }: { group: CartGroup; children?: ReactNode }) { + return ( +
+ + {group.items.length.toLocaleString("fa-IR")} کالا + + + + + {group.store.name} + + +
+ {group.items.map((item) => ( + + ))} +
+ +
+ + جمع فروشگاه: {group.items_subtotal.toLocaleString("fa-IR")} تومان + +
+ + {children} +
+ ); +} From 08e4a1a98a22b33948f285699649ac85b41fe17f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afra=20=E2=80=8C?= Date: Wed, 2 Sep 2026 10:40:24 +0330 Subject: [PATCH 11/12] feat: checkout page implemented --- src/components/customer/checkout-fulfillment.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/customer/checkout-fulfillment.tsx b/src/components/customer/checkout-fulfillment.tsx index 7ca7b10..a3e638a 100644 --- a/src/components/customer/checkout-fulfillment.tsx +++ b/src/components/customer/checkout-fulfillment.tsx @@ -94,7 +94,9 @@ export function CheckoutFulfillment({ group }: { group: CartGroup }) {
- + {/* Pickup has its own confirm+pay screen (no address needed); delivery still + goes to the general /checkout form until that flow's design is provided. */} + +
+
+ ); +}