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..30cae4d --- /dev/null +++ b/src/app/(customer)/(shop)/profile/(routes)/(landing)/page.tsx @@ -0,0 +1,11 @@ +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"; + +export default function ProfilePage() { + 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..5af139a --- /dev/null +++ b/src/app/(customer)/(shop)/profile/(routes)/wallet/page.tsx @@ -0,0 +1,16 @@ +import ProfilePageHeadLine from '@/components/profile/profile-page-head-line' +import TransactionHistory from '@/components/wallet/transaction-history' +import WalletBlancCard from '@/components/wallet/wallet-blanc-card' + +const WalletPage = () => { + return ( +
+ + + + +
+ ) +} + +export default WalletPage \ No newline at end of file diff --git a/src/app/(customer)/(shop)/profile/layout.tsx b/src/app/(customer)/(shop)/profile/layout.tsx new file mode 100644 index 0000000..dc8b338 --- /dev/null +++ b/src/app/(customer)/(shop)/profile/layout.tsx @@ -0,0 +1,28 @@ +import { requireSession } from "@/lib/auth/session"; +import { getAccount } from "@/lib/api/account"; +import { AccountProvider } from "@/lib/providers/account-provider"; + +export default async function DashboardLayout({ + children, +}: { + children: React.ReactNode; +}) { + await requireSession("/login"); + + let account: Awaited> | null = null; + let error: string | null = null; + + try { + account = await getAccount(); + } catch (e: any) { + error = e?.message ?? "خطا در دریافت اطلاعات پروفایل"; + } + + return ( +
+ + {children} + +
+ ); +} \ 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/app/layout.tsx b/src/app/layout.tsx index 60364b4..217ce00 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -15,8 +15,8 @@ export const metadata: Metadata = { export default function RootLayout({ children }: LayoutProps<"/">) { return ( - - + + {children} diff --git a/src/components/profile/profile-exit-account.tsx b/src/components/profile/profile-exit-account.tsx new file mode 100644 index 0000000..a5fec14 --- /dev/null +++ b/src/components/profile/profile-exit-account.tsx @@ -0,0 +1,35 @@ +'use client'; + +import { ChevronLeft, LogOutIcon } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useTransition } from "react"; + +const ProfileExitAccount = () => { + const router = useRouter(); + const [isPending, startTransition] = useTransition(); + + function handleLogout() { + startTransition(async () => { + await fetch("/api/auth/logout", { method: "POST" }); + router.push("/"); + router.refresh(); + }); + } + + return ( +
+
+
+ +
+ خروج +
+ +
+ ); +}; + +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..9196af6 --- /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..3f0843b --- /dev/null +++ b/src/components/profile/profile-menu.tsx @@ -0,0 +1,70 @@ +'use client'; + +import { + ShoppingBag, + MapPin, + Wallet, + Bell, + CircleQuestionMark, +} from 'lucide-react'; +import ProfileMenuItem from './profile-menu-item'; +import ProfileExitAccount from './profile-exit-account'; + +const menuItems = [ + { + id: 1, + href: '/addresses', + text: 'سعادت‌آباد', + icon: , + }, + { + id: 2, + href: '/orders', + text: 'سفارش‌های من', + icon: , + }, + { + id: 3, + href: '/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..57d83d4 --- /dev/null +++ b/src/components/profile/profile-person-detail-card.tsx @@ -0,0 +1,70 @@ +'use client'; +import { useAccount } from "@/lib/providers/account-provider"; +import { getDisplayNameHelper, getInitialsHelper } from "@/lib/utils/account-utils"; + +const ProfilePersonDetailCard = () => { + const { data, error, isLoading } = useAccount(); + + 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 new file mode 100644 index 0000000..38d9721 --- /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..6bf2c7c --- /dev/null +++ b/src/components/wallet/transaction-item.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { ArrowDownIcon, ArrowUp } from 'lucide-react'; +import { formatPriceWithToman } from '@/lib/utils/priceUtils'; + +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-success' : 'text-error'; + const bgColor = isDeposit ? 'bg-success/10' : 'bg-error/10'; + const iconColor = isDeposit ? 'text-success' : 'text-error'; + + return ( +
+
+
+ +
+ {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 new file mode 100644 index 0000000..26b073f --- /dev/null +++ b/src/components/wallet/wallet-blanc-card.tsx @@ -0,0 +1,52 @@ +'use client'; + +import { formatPriceWithToman } from '@/lib/utils/priceUtils'; +import { LogOutIcon, PlusIcon } from 'lucide-react'; + +const WalletBlancCard = () => { + return ( +
+

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

+

+ {formatPriceWithToman(35000000)} +

+ +
+ + + +
+
+ ); +}; + +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/lib/utils/priceUtils.ts b/src/lib/utils/priceUtils.ts new file mode 100644 index 0000000..1ce20ff --- /dev/null +++ b/src/lib/utils/priceUtils.ts @@ -0,0 +1,42 @@ +/** + * Converts Rial to Toman (divides by 10) + */ +export const convertRialToToman = (rialAmount: number): number => { + return Math.round(rialAmount / 10); +}; + +/** + * Formats number with comma separators (e.g., 3,000,000) + */ +export const formatNumberWithCommas = (amount: number): string => { + return amount.toLocaleString('fa-IR'); // for Persian digits + // Or for English digits: return amount.toLocaleString('en-US'); +}; + +/** + * Main function: receives amount in Rial or Toman, returns formatted with comma separators and "Toman" label + * + * @param amount - input amount (in Rial or Toman) + * @param inputIsRial - whether the input amount is in Rial (default: true) + * @returns string - formatted amount with Toman label (e.g., "۳,۰۰۰,۰۰۰ تومان") + */ +export const formatPriceWithToman = ( + amount: number, + inputIsRial: boolean = true +): string => { + // If amount is in Rial, convert to Toman first + const tomanAmount = inputIsRial ? convertRialToToman(amount) : amount; + + // Add comma separators + const formatted = formatNumberWithCommas(tomanAmount); + + // Return with Toman label + return `${formatted} تومان`; +}; + +/** + * Simpler version: just take an amount in Toman and format it + */ +export const formatTomanPrice = (tomanAmount: number): string => { + return `${formatNumberWithCommas(tomanAmount)} تومان`; +}; \ 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