feat: add customer authentication and order dashboard
This commit is contained in:
+20
-21
@@ -12,6 +12,9 @@ import {useAppSelector} from '@/store/hooks';
|
||||
import {selectCartCount} from '@/store/cartSlice';
|
||||
import {useLanguage} from '@/app/context/LanguageContext';
|
||||
import {commerceCopy} from '@/data/commerce';
|
||||
import {accountCopy} from '@/data/account';
|
||||
import {useAuth} from '@/app/context/AuthContext';
|
||||
import ProtectedLink from './auth/ProtectedLink';
|
||||
import ThemeToggle from './ThemeToggle';
|
||||
import LanguageToggle from './LanguageToggle';
|
||||
|
||||
@@ -19,6 +22,8 @@ export default function Header() {
|
||||
const pathname = usePathname();
|
||||
const cartCount = useAppSelector(selectCartCount);
|
||||
const {t, locale} = useLanguage();
|
||||
const auth = useAuth();
|
||||
const account = accountCopy[locale];
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
|
||||
@@ -28,6 +33,9 @@ export default function Header() {
|
||||
{href: '/#about', label: t.nav.about},
|
||||
{href: '/#locations', label: t.nav.locations},
|
||||
];
|
||||
const accountLabel = auth.session?.profile.firstName
|
||||
? `${auth.session.profile.firstName} ${auth.session.profile.lastName}`.trim()
|
||||
: auth.session?.profile.phone || account.login;
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => setScrolled(window.scrollY > 24);
|
||||
@@ -70,21 +78,17 @@ export default function Header() {
|
||||
<ThemeToggle/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="hidden sm:inline-flex"
|
||||
icon={<UserOutlined/>}
|
||||
href="https://panel.my-rm.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t.nav.login}
|
||||
</Button>
|
||||
{auth.isAuthenticated ? (
|
||||
<Button className="hidden sm:inline-flex" icon={<UserOutlined/>} href="/dashboard">{accountLabel}</Button>
|
||||
) : (
|
||||
<Button className="hidden sm:inline-flex" icon={<UserOutlined/>} onClick={() => auth.openLogin()}>{account.login}</Button>
|
||||
)}
|
||||
|
||||
<Badge count={cartCount} size="small" offset={[-2, 2]}>
|
||||
<Link href="/cart" onClick={() => setMobileOpen(false)} aria-label={t.cart.title}
|
||||
<ProtectedLink href="/cart" onClick={() => setMobileOpen(false)}
|
||||
className="w-11 h-11 rounded-full border border-subtle bg-elevated inline-flex items-center justify-center text-xl text-primary hover:text-brand-400">
|
||||
<ShoppingCartOutlined/>
|
||||
</Link>
|
||||
</ProtectedLink>
|
||||
</Badge>
|
||||
|
||||
<button
|
||||
@@ -113,16 +117,11 @@ export default function Header() {
|
||||
<div className="flex items-center gap-3 pt-2 border-t border-subtle">
|
||||
<LanguageToggle/>
|
||||
<ThemeToggle/>
|
||||
<Button
|
||||
icon={<UserOutlined/>}
|
||||
className="flex-1"
|
||||
href="https://panel.my-rm.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
>
|
||||
{t.nav.login}
|
||||
</Button>
|
||||
{auth.isAuthenticated ? (
|
||||
<Button icon={<UserOutlined/>} className="flex-1" href="/dashboard" onClick={() => setMobileOpen(false)}>{accountLabel}</Button>
|
||||
) : (
|
||||
<Button icon={<UserOutlined/>} className="flex-1" onClick={() => {setMobileOpen(false); auth.openLogin();}}>{account.login}</Button>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
)}
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
import {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {LeftOutlined, RightOutlined} from '@ant-design/icons';
|
||||
import {useLanguage} from '@/app/context/LanguageContext';
|
||||
import {accountCopy} from '@/data/account';
|
||||
|
||||
const AUTOPLAY_DELAY = 5000; // Advance every five seconds.
|
||||
|
||||
export default function HeroSection() {
|
||||
const {t, dir} = useLanguage();
|
||||
const {t, dir, locale} = useLanguage();
|
||||
const slides = t.hero.slides;
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
@@ -107,12 +108,15 @@ export default function HeroSection() {
|
||||
))}
|
||||
</h1>
|
||||
<p className="mt-5 text-white/90 text-base md:text-lg max-w-xl drop-shadow-lg">{slide.sub}</p>
|
||||
<a
|
||||
href="#showcase"
|
||||
className="mt-8 inline-flex items-center gap-2 bg-brand hover:bg-brand-400 text-ink-950 font-bold px-8 py-3.5 rounded-full transition-colors shadow-brandGlow"
|
||||
>
|
||||
{t.hero.cta}
|
||||
</a>
|
||||
<div className="mt-8 flex flex-wrap gap-3">
|
||||
<a href="#showcase" className="inline-flex items-center gap-2 bg-brand hover:bg-brand-400 text-ink-950 font-bold px-8 py-3.5 rounded-full transition-colors shadow-brandGlow">
|
||||
{t.hero.cta}
|
||||
</a>
|
||||
<a href="https://panel.my-rm.com" target="_blank" rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 bg-white/10 hover:bg-white/20 text-white border border-white/30 font-bold px-8 py-3.5 rounded-full transition-colors backdrop-blur-sm">
|
||||
{accountCopy[locale].robotMasterPanel}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,11 +7,13 @@ import {useAppDispatch, useAppSelector} from '@/store/hooks';
|
||||
import {addToCart, decrementQty} from '@/store/cartSlice';
|
||||
import {useLanguage} from '@/app/context/LanguageContext';
|
||||
import {commerceCopy} from '@/data/commerce';
|
||||
import {useAuth} from '@/app/context/AuthContext';
|
||||
|
||||
export default function QuantityControl({product}: {product: CartProduct}) {
|
||||
const dispatch = useAppDispatch();
|
||||
const {locale} = useLanguage();
|
||||
const copy = commerceCopy[locale];
|
||||
const auth = useAuth();
|
||||
const quantity = useAppSelector(state => state.cart.items.find(item => item.id === product.id)?.qty ?? 0);
|
||||
|
||||
return (
|
||||
@@ -19,14 +21,17 @@ export default function QuantityControl({product}: {product: CartProduct}) {
|
||||
className="inline-flex items-center gap-3 rounded-xl border border-subtle bg-page p-1" dir="ltr">
|
||||
<Button icon={<MinusOutlined/>} disabled={quantity === 0}
|
||||
aria-label={`${copy.decrease}: ${product.name}`}
|
||||
onClick={() => dispatch(decrementQty(product.id))}/>
|
||||
onClick={() => auth.isAuthenticated ? dispatch(decrementQty(product.id)) : auth.openLogin()}/>
|
||||
<span aria-live="polite" aria-atomic="true" className="min-w-8 text-center font-bold text-primary">
|
||||
{quantity.toLocaleString(locale === 'fa' ? 'fa-IR' : 'en-US')}
|
||||
</span>
|
||||
<Button type="primary" icon={<PlusOutlined/>}
|
||||
aria-label={`${copy.increase}: ${product.name}`}
|
||||
onClick={() => dispatch(addToCart({id: product.id, name: product.name, image: product.image,
|
||||
priceLabel: product.priceLabel, priceNumeric: product.priceNumeric}))}/>
|
||||
onClick={() => {
|
||||
const add = () => dispatch(addToCart({id: product.id, name: product.name, image: product.image,
|
||||
priceLabel: product.priceLabel, priceNumeric: product.priceNumeric}));
|
||||
if (auth.isAuthenticated) add(); else auth.openLogin(add);
|
||||
}}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
import type {Machine} from '@/types/domain';
|
||||
import type {MouseEvent} from 'react';
|
||||
import {useRef} from 'react';
|
||||
import Link from 'next/link';
|
||||
import QuantityControl from './QuantityControl';
|
||||
import ProtectedLink from './auth/ProtectedLink';
|
||||
import {commerceCopy} from '@/data/commerce';
|
||||
import {useLanguage} from '@/app/context/LanguageContext';
|
||||
import FloatingItems from './FloatingItems';
|
||||
@@ -89,9 +89,9 @@ export default function VendingCard({machine}: {machine: Machine}) {
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 mt-2 ">
|
||||
<QuantityControl product={machine}/>
|
||||
<Link href={`/products/${machine.id}`} className="px-5 py-3 rounded-xl border border-subtle text-primary hover:text-brand-400">
|
||||
<ProtectedLink href={`/products/${machine.id}`} className="px-5 py-3 rounded-xl border border-subtle text-primary hover:text-brand-400">
|
||||
{commerceCopy[locale].details}
|
||||
</Link>
|
||||
</ProtectedLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
'use client';
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
import {Alert, App, Button, Form, Input, Modal, Segmented, Typography} from 'antd';
|
||||
import {LockOutlined, MobileOutlined, SafetyCertificateOutlined} from '@ant-design/icons';
|
||||
import {useAuth} from '@/app/context/AuthContext';
|
||||
import {useLanguage} from '@/app/context/LanguageContext';
|
||||
import {accountCopy} from '@/data/account';
|
||||
|
||||
type LoginMode = 'otp' | 'password';
|
||||
|
||||
export default function AuthModal() {
|
||||
const auth = useAuth();
|
||||
const {message} = App.useApp();
|
||||
const {locale} = useLanguage();
|
||||
const copy = accountCopy[locale];
|
||||
const [mode, setMode] = useState<LoginMode>('otp');
|
||||
const [step, setStep] = useState<'phone' | 'code'>('phone');
|
||||
const [phone, setPhone] = useState('09123456789');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (auth.loginOpen) {
|
||||
setMode('otp');
|
||||
setStep('phone');
|
||||
setError('');
|
||||
}
|
||||
}, [auth.loginOpen]);
|
||||
|
||||
const submit = async (values: {phone?: string; code?: string; password?: string}) => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const currentPhone = (values.phone || phone).replace(/\s/g, '');
|
||||
if (mode === 'otp' && step === 'phone') {
|
||||
await auth.sendOtp(currentPhone);
|
||||
setPhone(currentPhone);
|
||||
setStep('code');
|
||||
message.success(copy.otpSent);
|
||||
} else if (mode === 'otp') {
|
||||
await auth.verifyOtp(currentPhone, values.code || '');
|
||||
} else {
|
||||
await auth.loginPassword(currentPhone, values.password || '');
|
||||
}
|
||||
} catch (caught) {
|
||||
setError(caught instanceof Error && caught.message === 'INVALID_PHONE' ? copy.invalidPhone : copy.invalidCredentials);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={auth.loginOpen} onCancel={auth.closeLogin} footer={null} centered title={copy.login} destroyOnClose>
|
||||
<Segmented<LoginMode> block value={mode} onChange={value => {setMode(value); setStep('phone'); setError('');}}
|
||||
options={[{label: copy.otpLogin, value: 'otp'}, {label: copy.passwordLogin, value: 'password'}]}/>
|
||||
<Alert className="mt-5" type="info" showIcon message={copy.demoTitle}
|
||||
description={<div><div>{copy.demoOtp}</div><div>{copy.demoPassword}</div></div>}/>
|
||||
{error && <Alert className="mt-4" type="error" showIcon message={error}/>}
|
||||
<Form layout="vertical" className="mt-5" onFinish={submit} initialValues={{phone: '09123456789', code: '12345', password: '123456'}}>
|
||||
{(mode === 'password' || step === 'phone') && (
|
||||
<Form.Item name="phone" label={copy.phone} rules={[{required: true, pattern: /^09\d{9}$/, message: copy.invalidPhone}]}>
|
||||
<Input dir="ltr" size="large" maxLength={11} prefix={<MobileOutlined/>}/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{mode === 'password' && (
|
||||
<Form.Item name="password" label={copy.password} rules={[{required: true}]}>
|
||||
<Input.Password dir="ltr" size="large" prefix={<LockOutlined/>}/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{mode === 'otp' && step === 'code' && (
|
||||
<>
|
||||
<Typography.Paragraph type="secondary">{copy.phone}: <span dir="ltr">{phone}</span></Typography.Paragraph>
|
||||
<Form.Item name="code" label={copy.otp} rules={[{required: true}]}>
|
||||
<Input dir="ltr" size="large" maxLength={5} prefix={<SafetyCertificateOutlined/>}/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
<Button htmlType="submit" type="primary" size="large" block loading={loading}>
|
||||
{mode === 'otp' ? (step === 'phone' ? copy.sendOtp : copy.verifyAndLogin) : copy.loginButton}
|
||||
</Button>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import type {MouseEvent, ReactNode} from 'react';
|
||||
import Link from 'next/link';
|
||||
import {useRouter} from 'next/navigation';
|
||||
import {useAuth} from '@/app/context/AuthContext';
|
||||
|
||||
export default function ProtectedLink({href, children, className, onClick}: {href: string; children: ReactNode; className?: string; onClick?: () => void}) {
|
||||
const auth = useAuth();
|
||||
const router = useRouter();
|
||||
const handleClick = (event: MouseEvent<HTMLAnchorElement>) => {
|
||||
onClick?.();
|
||||
if (!auth.isAuthenticated) {
|
||||
event.preventDefault();
|
||||
auth.openLogin(() => router.push(href));
|
||||
}
|
||||
};
|
||||
return <Link href={href} className={className} onClick={handleClick}>{children}</Link>;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
'use client';
|
||||
|
||||
import type {ReactNode} from 'react';
|
||||
import {useEffect, useRef} from 'react';
|
||||
import {Button, Skeleton} from 'antd';
|
||||
import {LockOutlined} from '@ant-design/icons';
|
||||
import {useAuth} from '@/app/context/AuthContext';
|
||||
import {useLanguage} from '@/app/context/LanguageContext';
|
||||
import {accountCopy} from '@/data/account';
|
||||
|
||||
export default function ProtectedPage({children}: {children: ReactNode}) {
|
||||
const auth = useAuth();
|
||||
const {locale} = useLanguage();
|
||||
const copy = accountCopy[locale];
|
||||
const prompted = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (auth.ready && !auth.isAuthenticated && !prompted.current) {
|
||||
prompted.current = true;
|
||||
auth.openLogin();
|
||||
}
|
||||
}, [auth]);
|
||||
|
||||
if (!auth.ready) return <main className="max-w-5xl mx-auto min-h-[70vh] px-5 pt-32"><Skeleton active/></main>;
|
||||
if (!auth.isAuthenticated) return (
|
||||
<main className="max-w-3xl mx-auto min-h-[70vh] px-5 pt-32 pb-20 text-center">
|
||||
<div className="rounded-3xl border border-subtle bg-surface p-10">
|
||||
<LockOutlined className="text-4xl text-brand-400"/>
|
||||
<h1 className="text-2xl font-extrabold text-primary mt-5">{copy.protectedTitle}</h1>
|
||||
<p className="text-secondary mt-3 mb-7">{copy.protectedDescription}</p>
|
||||
<Button type="primary" size="large" onClick={() => auth.openLogin()}>{copy.openLogin}</Button>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
return children;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
'use client';
|
||||
|
||||
import type {ReactNode} from 'react';
|
||||
import Link from 'next/link';
|
||||
import {usePathname, useRouter} from 'next/navigation';
|
||||
import {Button} from 'antd';
|
||||
import {DashboardOutlined, FileTextOutlined, LogoutOutlined, UserOutlined} from '@ant-design/icons';
|
||||
import {useAuth} from '@/app/context/AuthContext';
|
||||
import {useLanguage} from '@/app/context/LanguageContext';
|
||||
import {accountCopy} from '@/data/account';
|
||||
|
||||
export default function DashboardShell({children}: {children: ReactNode}) {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const auth = useAuth();
|
||||
const {locale} = useLanguage();
|
||||
const copy = accountCopy[locale];
|
||||
const links = [
|
||||
{href: '/dashboard', label: copy.dashboard, icon: <DashboardOutlined/>},
|
||||
{href: '/dashboard/orders', label: copy.orders, icon: <FileTextOutlined/>},
|
||||
{href: '/dashboard/profile', label: copy.profile, icon: <UserOutlined/>},
|
||||
];
|
||||
const active = (href: string) => href === '/dashboard' ? pathname === href : pathname.startsWith(href);
|
||||
return (
|
||||
<main className="max-w-7xl mx-auto min-h-[75vh] px-5 md:px-8 pt-28 pb-20">
|
||||
<div className="grid lg:grid-cols-[240px,minmax(0,1fr)] gap-7 items-start">
|
||||
<aside className="lg:sticky lg:top-28 rounded-2xl border border-subtle bg-elevated p-4">
|
||||
<div className="px-3 py-3 border-b border-subtle mb-3">
|
||||
<p className="font-bold text-primary">{auth.session?.profile.firstName || copy.account}</p>
|
||||
<p className="text-sm text-secondary mt-1" dir="ltr">{auth.session?.profile.phone}</p>
|
||||
</div>
|
||||
<nav className="flex lg:flex-col gap-2 overflow-x-auto no-scrollbar">
|
||||
{links.map(link => <Link key={link.href} href={link.href}
|
||||
className={`flex items-center gap-2 whitespace-nowrap px-4 py-3 rounded-xl transition-colors ${active(link.href) ? 'bg-brand text-ink-950 font-bold' : 'text-secondary hover:bg-surface-strong hover:text-primary'}`}>
|
||||
{link.icon}{link.label}
|
||||
</Link>)}
|
||||
</nav>
|
||||
<Button className="mt-3" type="text" danger icon={<LogoutOutlined/>} onClick={() => {auth.logout(); router.push('/');}} block>{copy.logout}</Button>
|
||||
</aside>
|
||||
<section className="min-w-0">{children}</section>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
import {forwardRef} from 'react';
|
||||
import type {CustomerOrder} from '@/types/account';
|
||||
import {useLanguage} from '@/app/context/LanguageContext';
|
||||
import {accountCopy} from '@/data/account';
|
||||
|
||||
const InvoiceDocument = forwardRef<HTMLDivElement, {order: CustomerOrder}>(({order}, ref) => {
|
||||
const {locale} = useLanguage();
|
||||
const copy = accountCopy[locale];
|
||||
const invoice = order.invoice!;
|
||||
const numberLocale = locale === 'fa' ? 'fa-IR' : 'en-US';
|
||||
const money = (value: number) => `${value.toLocaleString(numberLocale)} ${locale === 'fa' ? 'ریال' : 'Rials'}`;
|
||||
const subtotal = order.items.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0);
|
||||
const total = subtotal - invoice.discount + invoice.shipping + invoice.tax;
|
||||
const buyerName = order.customer.type === 'corporate' ? order.customer.companyName : `${order.customer.firstName} ${order.customer.lastName}`;
|
||||
return (
|
||||
<div ref={ref} className="invoice-document" dir={locale === 'fa' ? 'rtl' : 'ltr'}>
|
||||
<div className="invoice-head">
|
||||
<img src="/images/icons/logo.png" alt="Robot Market"/>
|
||||
<div><h2>{copy.invoice}</h2><p>{copy.invoiceNumber}: <span dir="ltr">{invoice.number}</span></p></div>
|
||||
</div>
|
||||
<div className="invoice-meta">
|
||||
<div><strong>{copy.seller}</strong><p>{copy.sellerName}</p><p>{copy.sellerPhone}</p></div>
|
||||
<div><strong>{copy.buyer}</strong><p>{buyerName}</p><p dir="ltr">{order.customer.phone}</p><p>{order.customer.city}، {order.customer.address}</p></div>
|
||||
<div><p>{copy.issueDate}: {invoice.issueDate}</p><p>{copy.validUntil}: {invoice.validUntil}</p></div>
|
||||
</div>
|
||||
<table><thead><tr><th>#</th><th>{copy.orderItems}</th><th>{copy.modules}</th><th>{copy.quantity}</th><th>{copy.unitPrice}</th><th>{copy.total}</th></tr></thead>
|
||||
<tbody>{order.items.map((item, index) => <tr key={item.id}><td>{index + 1}</td><td>{item.name}<small>{item.model}</small></td><td>{item.modules.join('، ')}</td><td>{item.quantity}</td><td>{money(item.unitPrice)}</td><td>{money(item.unitPrice * item.quantity)}</td></tr>)}</tbody>
|
||||
</table>
|
||||
<div className="invoice-totals">
|
||||
<p><span>{copy.subtotal}</span><strong>{money(subtotal)}</strong></p>
|
||||
<p><span>{copy.discount}</span><strong>{money(invoice.discount)}</strong></p>
|
||||
<p><span>{copy.shipping}</span><strong>{money(invoice.shipping)}</strong></p>
|
||||
<p><span>{copy.tax}</span><strong>{money(invoice.tax)}</strong></p>
|
||||
<p className="grand"><span>{copy.finalTotal}</span><strong>{money(total)}</strong></p>
|
||||
</div>
|
||||
<p className="invoice-note">این پیشفاکتور پس از هماهنگی تلفنی و تأیید ادمین روبات مارکت صادر شده است.</p>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
InvoiceDocument.displayName = 'InvoiceDocument';
|
||||
export default InvoiceDocument;
|
||||
@@ -0,0 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import type {CustomerOrder} from '@/types/account';
|
||||
import {useLanguage} from '@/app/context/LanguageContext';
|
||||
import {accountCopy, paymentStatusLabels} from '@/data/account';
|
||||
import OrderStatusTag from './OrderStatusTag';
|
||||
|
||||
export default function OrderCard({order}: {order: CustomerOrder}) {
|
||||
const {locale} = useLanguage();
|
||||
const copy = accountCopy[locale];
|
||||
return (
|
||||
<article className="rounded-2xl border border-subtle bg-elevated p-5">
|
||||
<div className="flex flex-wrap justify-between gap-4">
|
||||
<div><p className="text-sm text-secondary">{copy.orderNumber}</p><strong className="text-primary" dir="ltr">{order.number}</strong></div>
|
||||
<OrderStatusTag status={order.status}/>
|
||||
</div>
|
||||
<div className="grid sm:grid-cols-3 gap-4 mt-5 pt-5 border-t border-subtle text-sm">
|
||||
<div><span className="text-secondary block">{copy.orderDate}</span><span className="text-primary">{order.createdAt}</span></div>
|
||||
<div><span className="text-secondary block">{copy.paymentStatus}</span><span className="text-primary">{paymentStatusLabels[locale][order.paymentStatus]}</span></div>
|
||||
<div><span className="text-secondary block">{copy.orderItems}</span><span className="text-primary">{order.items.reduce((sum, item) => sum + item.quantity, 0).toLocaleString(locale === 'fa' ? 'fa-IR' : 'en-US')}</span></div>
|
||||
</div>
|
||||
<Link href={`/dashboard/orders/${order.id}`} className="inline-block text-brand-400 font-semibold mt-5 hover:underline">{copy.details}</Link>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import {Tag} from 'antd';
|
||||
import type {OrderStatus} from '@/types/account';
|
||||
import {useLanguage} from '@/app/context/LanguageContext';
|
||||
import {orderStatusLabels} from '@/data/account';
|
||||
|
||||
export default function OrderStatusTag({status}: {status: OrderStatus}) {
|
||||
const {locale} = useLanguage();
|
||||
const color = status === 'cancelled' ? 'red' : status === 'delivered' ? 'green' : status === 'proforma_issued' ? 'cyan' : 'gold';
|
||||
return <Tag color={color}>{orderStatusLabels[locale][status]}</Tag>;
|
||||
}
|
||||
Reference in New Issue
Block a user