chore: format project automatically before starting dev server
This commit is contained in:
+5
-1
@@ -5,5 +5,9 @@ import ProtectedPage from '@/components/auth/ProtectedPage';
|
||||
export const metadata: Metadata = {title: 'سبد خرید | روبات مارکت'};
|
||||
|
||||
export default function Page() {
|
||||
return <ProtectedPage><CartPage/></ProtectedPage>;
|
||||
return (
|
||||
<ProtectedPage>
|
||||
<CartPage />
|
||||
</ProtectedPage>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,13 +51,16 @@ export function AuthProvider({children}: {children: ReactNode}) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const finishLogin = useCallback((next: AuthSession) => {
|
||||
persist(next);
|
||||
setLoginOpen(false);
|
||||
const callback = successCallback.current;
|
||||
successCallback.current = undefined;
|
||||
callback?.();
|
||||
}, [persist]);
|
||||
const finishLogin = useCallback(
|
||||
(next: AuthSession) => {
|
||||
persist(next);
|
||||
setLoginOpen(false);
|
||||
const callback = successCallback.current;
|
||||
successCallback.current = undefined;
|
||||
callback?.();
|
||||
},
|
||||
[persist],
|
||||
);
|
||||
|
||||
const openLogin = useCallback((onSuccess?: () => void) => {
|
||||
successCallback.current = onSuccess;
|
||||
@@ -76,7 +79,9 @@ export function AuthProvider({children}: {children: ReactNode}) {
|
||||
loginOpen,
|
||||
openLogin,
|
||||
closeLogin,
|
||||
sendOtp: async phone => { await requestOtp(phone); },
|
||||
sendOtp: async phone => {
|
||||
await requestOtp(phone);
|
||||
},
|
||||
verifyOtp: async (phone, code) => finishLogin(await loginWithOtp(phone, code)),
|
||||
loginPassword: async (phone, password) => finishLogin(await loginWithPassword(phone, password)),
|
||||
updateProfile: async profile => {
|
||||
@@ -90,7 +95,7 @@ export function AuthProvider({children}: {children: ReactNode}) {
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
<AuthModal/>
|
||||
<AuthModal />
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export function LanguageProvider({children}: {children: ReactNode}) {
|
||||
window.localStorage.setItem('rm-locale', locale);
|
||||
}, [locale]);
|
||||
|
||||
const toggleLocale = () => setLocale((l) => (l === 'fa' ? 'en' : 'fa'));
|
||||
const toggleLocale = () => setLocale(l => (l === 'fa' ? 'en' : 'fa'));
|
||||
|
||||
return (
|
||||
<LanguageContext.Provider
|
||||
|
||||
@@ -14,11 +14,9 @@ interface ThemeContextValue {
|
||||
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
||||
|
||||
export function ThemeProvider({children}: {children: ReactNode}) {
|
||||
|
||||
const [theme, setTheme] = useState<Theme>('light');
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const saved = window.localStorage.getItem('rm-theme');
|
||||
if (saved === 'light' || saved === 'dark') {
|
||||
@@ -29,7 +27,6 @@ export function ThemeProvider({children}: {children: ReactNode}) {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
root.classList.remove('light', 'dark');
|
||||
@@ -37,13 +34,9 @@ export function ThemeProvider({children}: {children: ReactNode}) {
|
||||
window.localStorage.setItem('rm-theme', theme);
|
||||
}, [theme]);
|
||||
|
||||
const toggleTheme = () => setTheme((t) => (t === 'dark' ? 'light' : 'dark'));
|
||||
const toggleTheme = () => setTheme(t => (t === 'dark' ? 'light' : 'dark'));
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{theme, setTheme, toggleTheme, mounted}}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
return <ThemeContext.Provider value={{theme, setTheme, toggleTheme, mounted}}>{children}</ThemeContext.Provider>;
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
|
||||
@@ -3,5 +3,9 @@ import ProtectedPage from '@/components/auth/ProtectedPage';
|
||||
import DashboardShell from '@/components/dashboard/DashboardShell';
|
||||
|
||||
export default function Layout({children}: {children: ReactNode}) {
|
||||
return <ProtectedPage><DashboardShell>{children}</DashboardShell></ProtectedPage>;
|
||||
return (
|
||||
<ProtectedPage>
|
||||
<DashboardShell>{children}</DashboardShell>
|
||||
</ProtectedPage>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,57 +23,143 @@ export default function OrderDetailsPage({params}: {params: {id: string}}) {
|
||||
const invoiceRef = useRef<HTMLDivElement>(null);
|
||||
const numberLocale = locale === 'fa' ? 'fa-IR' : 'en-US';
|
||||
const money = (value: number) => `${value.toLocaleString(numberLocale)} ${locale === 'fa' ? 'ریال' : 'Rials'}`;
|
||||
useEffect(() => { if (auth.session) getOrder(params.id, auth.session.profile).then(setOrder); }, [auth.session, params.id]);
|
||||
if (order === null) return <Skeleton active/>;
|
||||
if (!order) return <div><Empty description={copy.notFound}/><Link href="/dashboard/orders" className="text-brand-400 block text-center mt-5">{copy.backToOrders}</Link></div>;
|
||||
useEffect(() => {
|
||||
if (auth.session) getOrder(params.id, auth.session.profile).then(setOrder);
|
||||
}, [auth.session, params.id]);
|
||||
if (order === null) return <Skeleton active />;
|
||||
if (!order)
|
||||
return (
|
||||
<div>
|
||||
<Empty description={copy.notFound} />
|
||||
<Link href="/dashboard/orders" className="text-brand-400 block text-center mt-5">
|
||||
{copy.backToOrders}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
const exportPdf = async () => {
|
||||
if (!invoiceRef.current) return;
|
||||
setExporting('pdf');
|
||||
try { await downloadInvoicePdf(invoiceRef.current, order.invoice!.number); }
|
||||
catch { await message.error('ساخت فایل PDF انجام نشد. دوباره تلاش کنید.'); }
|
||||
finally { setExporting(null); }
|
||||
try {
|
||||
await downloadInvoicePdf(invoiceRef.current, order.invoice!.number);
|
||||
} catch {
|
||||
await message.error('ساخت فایل PDF انجام نشد. دوباره تلاش کنید.');
|
||||
} finally {
|
||||
setExporting(null);
|
||||
}
|
||||
};
|
||||
const exportExcel = async () => {
|
||||
setExporting('excel');
|
||||
try { await downloadInvoiceExcel(order, order.invoice!.number); }
|
||||
catch { await message.error('ساخت فایل Excel انجام نشد. دوباره تلاش کنید.'); }
|
||||
finally { setExporting(null); }
|
||||
try {
|
||||
await downloadInvoiceExcel(order, order.invoice!.number);
|
||||
} catch {
|
||||
await message.error('ساخت فایل Excel انجام نشد. دوباره تلاش کنید.');
|
||||
} finally {
|
||||
setExporting(null);
|
||||
}
|
||||
};
|
||||
return <>
|
||||
<Link href="/dashboard/orders" className="text-brand-400 hover:underline">{copy.backToOrders}</Link>
|
||||
<div className="flex flex-wrap justify-between gap-4 items-center mt-5">
|
||||
<div><p className="text-secondary">{copy.orderNumber}</p><h1 className="text-3xl font-extrabold text-primary" dir="ltr">{order.number}</h1></div>
|
||||
<OrderStatusTag status={order.status}/>
|
||||
</div>
|
||||
<div className="grid sm:grid-cols-3 gap-4 mt-7">
|
||||
<div className="rounded-xl border border-subtle bg-elevated p-4"><span className="text-sm text-secondary">{copy.orderDate}</span><strong className="block text-primary mt-1">{order.createdAt}</strong></div>
|
||||
<div className="rounded-xl border border-subtle bg-elevated p-4"><span className="text-sm text-secondary">{copy.lastUpdate}</span><strong className="block text-primary mt-1">{order.updatedAt}</strong></div>
|
||||
<div className="rounded-xl border border-subtle bg-elevated p-4"><span className="text-sm text-secondary">{copy.paymentStatus}</span><strong className="block text-primary mt-1">{paymentStatusLabels[locale][order.paymentStatus]}</strong></div>
|
||||
</div>
|
||||
|
||||
<section className="rounded-2xl border border-subtle bg-elevated p-5 md:p-7 mt-7">
|
||||
<h2 className="text-xl font-bold text-primary mb-6">{copy.timeline}</h2>
|
||||
<Steps direction="vertical" current={order.events.length - 1} items={order.events.map(event => ({title: orderStatusLabels[locale][event.status], description: `${event.date} — ${event.note}`, status: event.completed ? 'finish' : 'wait'}))}/>
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl border border-subtle bg-elevated p-5 md:p-7 mt-7">
|
||||
<h2 className="text-xl font-bold text-primary mb-5">{copy.orderItems}</h2>
|
||||
<div className="space-y-5">{order.items.map(item => <article key={item.id} className="flex flex-wrap sm:flex-nowrap gap-5 border-b border-subtle last:border-0 pb-5 last:pb-0">
|
||||
<img src={item.image} alt={item.name} className="w-24 h-28 object-contain rounded-xl bg-surface"/>
|
||||
<div className="flex-1"><h3 className="font-bold text-primary">{item.name}</h3><p className="text-sm text-secondary mt-1" dir="ltr">{item.model}</p><p className="text-secondary mt-3">{copy.modules}: {item.modules.join('، ')}</p><p className="text-primary mt-3">{copy.quantity}: {item.quantity.toLocaleString(numberLocale)} · {copy.total}: {money(item.quantity * item.unitPrice)}</p></div>
|
||||
</article>)}</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-7">
|
||||
<h2 className="text-xl font-bold text-primary mb-5">{copy.invoice}</h2>
|
||||
{!order.invoice ? <Alert type="info" showIcon message={copy.invoicePending}/> : <>
|
||||
<div className="flex flex-wrap gap-3 mb-5 invoice-actions">
|
||||
<Button type="primary" icon={<DownloadOutlined/>} loading={exporting === 'pdf'} onClick={exportPdf}>{copy.downloadPdf}</Button>
|
||||
<Button icon={<FileExcelOutlined/>} loading={exporting === 'excel'} onClick={exportExcel}>{copy.downloadExcel}</Button>
|
||||
<Button icon={<PrinterOutlined/>} onClick={() => window.print()}>{copy.print}</Button>
|
||||
return (
|
||||
<>
|
||||
<Link href="/dashboard/orders" className="text-brand-400 hover:underline">
|
||||
{copy.backToOrders}
|
||||
</Link>
|
||||
<div className="flex flex-wrap justify-between gap-4 items-center mt-5">
|
||||
<div>
|
||||
<p className="text-secondary">{copy.orderNumber}</p>
|
||||
<h1 className="text-3xl font-extrabold text-primary" dir="ltr">
|
||||
{order.number}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="overflow-x-auto rounded-2xl border border-subtle"><InvoiceDocument ref={invoiceRef} order={order}/></div>
|
||||
</>}
|
||||
</section>
|
||||
</>;
|
||||
<OrderStatusTag status={order.status} />
|
||||
</div>
|
||||
<div className="grid sm:grid-cols-3 gap-4 mt-7">
|
||||
<div className="rounded-xl border border-subtle bg-elevated p-4">
|
||||
<span className="text-sm text-secondary">{copy.orderDate}</span>
|
||||
<strong className="block text-primary mt-1">{order.createdAt}</strong>
|
||||
</div>
|
||||
<div className="rounded-xl border border-subtle bg-elevated p-4">
|
||||
<span className="text-sm text-secondary">{copy.lastUpdate}</span>
|
||||
<strong className="block text-primary mt-1">{order.updatedAt}</strong>
|
||||
</div>
|
||||
<div className="rounded-xl border border-subtle bg-elevated p-4">
|
||||
<span className="text-sm text-secondary">{copy.paymentStatus}</span>
|
||||
<strong className="block text-primary mt-1">
|
||||
{paymentStatusLabels[locale][order.paymentStatus]}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="rounded-2xl border border-subtle bg-elevated p-5 md:p-7 mt-7">
|
||||
<h2 className="text-xl font-bold text-primary mb-6">{copy.timeline}</h2>
|
||||
<Steps
|
||||
direction="vertical"
|
||||
current={order.events.length - 1}
|
||||
items={order.events.map(event => ({
|
||||
title: orderStatusLabels[locale][event.status],
|
||||
description: `${event.date} — ${event.note}`,
|
||||
status: event.completed ? 'finish' : 'wait',
|
||||
}))}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl border border-subtle bg-elevated p-5 md:p-7 mt-7">
|
||||
<h2 className="text-xl font-bold text-primary mb-5">{copy.orderItems}</h2>
|
||||
<div className="space-y-5">
|
||||
{order.items.map(item => (
|
||||
<article
|
||||
key={item.id}
|
||||
className="flex flex-wrap sm:flex-nowrap gap-5 border-b border-subtle last:border-0 pb-5 last:pb-0"
|
||||
>
|
||||
<img
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
className="w-24 h-28 object-contain rounded-xl bg-surface"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-bold text-primary">{item.name}</h3>
|
||||
<p className="text-sm text-secondary mt-1" dir="ltr">
|
||||
{item.model}
|
||||
</p>
|
||||
<p className="text-secondary mt-3">
|
||||
{copy.modules}: {item.modules.join('، ')}
|
||||
</p>
|
||||
<p className="text-primary mt-3">
|
||||
{copy.quantity}: {item.quantity.toLocaleString(numberLocale)} · {copy.total}:{' '}
|
||||
{money(item.quantity * item.unitPrice)}
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-7">
|
||||
<h2 className="text-xl font-bold text-primary mb-5">{copy.invoice}</h2>
|
||||
{!order.invoice ? (
|
||||
<Alert type="info" showIcon message={copy.invoicePending} />
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-3 mb-5 invoice-actions">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<DownloadOutlined />}
|
||||
loading={exporting === 'pdf'}
|
||||
onClick={exportPdf}
|
||||
>
|
||||
{copy.downloadPdf}
|
||||
</Button>
|
||||
<Button icon={<FileExcelOutlined />} loading={exporting === 'excel'} onClick={exportExcel}>
|
||||
{copy.downloadExcel}
|
||||
</Button>
|
||||
<Button icon={<PrinterOutlined />} onClick={() => window.print()}>
|
||||
{copy.print}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="overflow-x-auto rounded-2xl border border-subtle">
|
||||
<InvoiceDocument ref={invoiceRef} order={order} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,10 +14,24 @@ export default function OrdersPage() {
|
||||
const {locale} = useLanguage();
|
||||
const copy = accountCopy[locale];
|
||||
const [orders, setOrders] = useState<CustomerOrder[] | null>(null);
|
||||
useEffect(() => { if (auth.session) getOrders(auth.session.profile).then(setOrders); }, [auth.session]);
|
||||
return <>
|
||||
<h1 className="text-3xl font-extrabold text-primary">{copy.orders}</h1>
|
||||
<p className="text-secondary mt-3 mb-7">{copy.dashboardDescription}</p>
|
||||
{!orders ? <Skeleton active/> : orders.length ? <div className="space-y-4">{orders.map(order => <OrderCard key={order.id} order={order}/>)}</div> : <Empty description={copy.emptyOrders}/>}
|
||||
</>;
|
||||
useEffect(() => {
|
||||
if (auth.session) getOrders(auth.session.profile).then(setOrders);
|
||||
}, [auth.session]);
|
||||
return (
|
||||
<>
|
||||
<h1 className="text-3xl font-extrabold text-primary">{copy.orders}</h1>
|
||||
<p className="text-secondary mt-3 mb-7">{copy.dashboardDescription}</p>
|
||||
{!orders ? (
|
||||
<Skeleton active />
|
||||
) : orders.length ? (
|
||||
<div className="space-y-4">
|
||||
{orders.map(order => (
|
||||
<OrderCard key={order.id} order={order} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Empty description={copy.emptyOrders} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+58
-16
@@ -16,27 +16,69 @@ export default function DashboardPage() {
|
||||
const {locale} = useLanguage();
|
||||
const copy = accountCopy[locale];
|
||||
const [orders, setOrders] = useState<CustomerOrder[] | null>(null);
|
||||
useEffect(() => { if (auth.session) getOrders(auth.session.profile).then(setOrders); }, [auth.session]);
|
||||
useEffect(() => {
|
||||
if (auth.session) getOrders(auth.session.profile).then(setOrders);
|
||||
}, [auth.session]);
|
||||
const completion = useMemo(() => {
|
||||
if (!auth.session) return 0;
|
||||
const profile = auth.session.profile;
|
||||
const required = [profile.firstName, profile.lastName, profile.province, profile.city, profile.address, profile.postalCode];
|
||||
return Math.round(required.filter(Boolean).length / required.length * 100);
|
||||
const required = [
|
||||
profile.firstName,
|
||||
profile.lastName,
|
||||
profile.province,
|
||||
profile.city,
|
||||
profile.address,
|
||||
profile.postalCode,
|
||||
];
|
||||
return Math.round((required.filter(Boolean).length / required.length) * 100);
|
||||
}, [auth.session]);
|
||||
if (!orders) return <Skeleton active/>;
|
||||
if (!orders) return <Skeleton active />;
|
||||
const activeOrders = orders.filter(order => !['delivered', 'cancelled'].includes(order.status)).length;
|
||||
const cards = [
|
||||
{label: copy.profileCompletion, value: `${completion.toLocaleString(locale === 'fa' ? 'fa-IR' : 'en-US')}٪`, icon: <CheckCircleOutlined/>, extra: <Progress percent={completion} showInfo={false} size="small"/>},
|
||||
{label: copy.activeOrders, value: activeOrders.toLocaleString(locale === 'fa' ? 'fa-IR' : 'en-US'), icon: <ClockCircleOutlined/>},
|
||||
{label: copy.totalOrders, value: orders.length.toLocaleString(locale === 'fa' ? 'fa-IR' : 'en-US'), icon: <FileTextOutlined/>},
|
||||
{
|
||||
label: copy.profileCompletion,
|
||||
value: `${completion.toLocaleString(locale === 'fa' ? 'fa-IR' : 'en-US')}٪`,
|
||||
icon: <CheckCircleOutlined />,
|
||||
extra: <Progress percent={completion} showInfo={false} size="small" />,
|
||||
},
|
||||
{
|
||||
label: copy.activeOrders,
|
||||
value: activeOrders.toLocaleString(locale === 'fa' ? 'fa-IR' : 'en-US'),
|
||||
icon: <ClockCircleOutlined />,
|
||||
},
|
||||
{
|
||||
label: copy.totalOrders,
|
||||
value: orders.length.toLocaleString(locale === 'fa' ? 'fa-IR' : 'en-US'),
|
||||
icon: <FileTextOutlined />,
|
||||
},
|
||||
];
|
||||
return <>
|
||||
<h1 className="text-3xl font-extrabold text-primary">{copy.welcome} {auth.session?.profile.firstName}</h1>
|
||||
<p className="text-secondary mt-3">{copy.dashboardDescription}</p>
|
||||
<div className="grid sm:grid-cols-3 gap-4 mt-8">{cards.map(card => <div key={card.label} className="rounded-2xl border border-subtle bg-elevated p-5">
|
||||
<div className="text-brand-400 text-2xl">{card.icon}</div><p className="text-secondary text-sm mt-4">{card.label}</p><strong className="text-2xl text-primary block mt-1">{card.value}</strong>{card.extra && <div className="mt-3">{card.extra}</div>}
|
||||
</div>)}</div>
|
||||
<div className="flex items-center justify-between gap-4 mt-10 mb-5"><h2 className="text-xl font-bold text-primary">{copy.recentOrders}</h2><Link href="/dashboard/orders" className="text-brand-400">{copy.viewAll}</Link></div>
|
||||
<div className="space-y-4">{orders.slice(0, 2).map(order => <OrderCard key={order.id} order={order}/>)}</div>
|
||||
</>;
|
||||
return (
|
||||
<>
|
||||
<h1 className="text-3xl font-extrabold text-primary">
|
||||
{copy.welcome} {auth.session?.profile.firstName}
|
||||
</h1>
|
||||
<p className="text-secondary mt-3">{copy.dashboardDescription}</p>
|
||||
<div className="grid sm:grid-cols-3 gap-4 mt-8">
|
||||
{cards.map(card => (
|
||||
<div key={card.label} className="rounded-2xl border border-subtle bg-elevated p-5">
|
||||
<div className="text-brand-400 text-2xl">{card.icon}</div>
|
||||
<p className="text-secondary text-sm mt-4">{card.label}</p>
|
||||
<strong className="text-2xl text-primary block mt-1">{card.value}</strong>
|
||||
{card.extra && <div className="mt-3">{card.extra}</div>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4 mt-10 mb-5">
|
||||
<h2 className="text-xl font-bold text-primary">{copy.recentOrders}</h2>
|
||||
<Link href="/dashboard/orders" className="text-brand-400">
|
||||
{copy.viewAll}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{orders.slice(0, 2).map(order => (
|
||||
<OrderCard key={order.id} order={order} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,34 +15,91 @@ export default function ProfilePage() {
|
||||
const [form] = Form.useForm<CustomerProfile>();
|
||||
const [saving, setSaving] = useState(false);
|
||||
const customerType = Form.useWatch('type', form);
|
||||
useEffect(() => { if (auth.session) form.setFieldsValue(auth.session.profile); }, [auth.session, form]);
|
||||
useEffect(() => {
|
||||
if (auth.session) form.setFieldsValue(auth.session.profile);
|
||||
}, [auth.session, form]);
|
||||
const submit = async (values: CustomerProfile) => {
|
||||
setSaving(true);
|
||||
try { await auth.updateProfile(values); message.success(copy.saved); } finally { setSaving(false); }
|
||||
try {
|
||||
await auth.updateProfile(values);
|
||||
message.success(copy.saved);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
return <>
|
||||
<h1 className="text-3xl font-extrabold text-primary">{copy.profile}</h1>
|
||||
<p className="text-secondary mt-3 mb-7">{copy.invoicePending}</p>
|
||||
<Form form={form} layout="vertical" onFinish={submit} className="rounded-2xl border border-subtle bg-elevated p-5 md:p-7">
|
||||
<Form.Item name="type" label={copy.customerType} rules={[{required: true}]}><Radio.Group optionType="button" buttonStyle="solid" options={[{label: copy.individual, value: 'individual'}, {label: copy.corporate, value: 'corporate'}]}/></Form.Item>
|
||||
<div className="grid md:grid-cols-2 gap-x-5">
|
||||
<Form.Item name="firstName" label={copy.firstName} rules={[{required: true}]}><Input size="large"/></Form.Item>
|
||||
<Form.Item name="lastName" label={copy.lastName} rules={[{required: true}]}><Input size="large"/></Form.Item>
|
||||
<Form.Item name="phone" label={copy.phone}><Input size="large" dir="ltr" disabled/></Form.Item>
|
||||
<Form.Item name="email" label={copy.email} rules={[{type: 'email'}]}><Input size="large" dir="ltr"/></Form.Item>
|
||||
{customerType === 'corporate' && <>
|
||||
<Form.Item name="companyName" label={copy.companyName} rules={[{required: true}]}><Input size="large"/></Form.Item>
|
||||
<Form.Item name="nationalId" label={copy.nationalId} rules={[{required: true}]}><Input size="large" dir="ltr"/></Form.Item>
|
||||
<Form.Item name="economicCode" label={copy.economicCode}><Input size="large" dir="ltr"/></Form.Item>
|
||||
</>}
|
||||
<Form.Item name="province" label={copy.province} rules={[{required: true}]}><Input size="large"/></Form.Item>
|
||||
<Form.Item name="city" label={copy.city} rules={[{required: true}]}><Input size="large"/></Form.Item>
|
||||
<Form.Item name="postalCode" label={copy.postalCode} rules={[{required: true}]}><Input size="large" dir="ltr" maxLength={10}/></Form.Item>
|
||||
<Form.Item name="preferredCallTime" label={copy.preferredCallTime}><Input size="large"/></Form.Item>
|
||||
</div>
|
||||
<Form.Item name="address" label={copy.address} rules={[{required: true}]}><Input.TextArea rows={4}/></Form.Item>
|
||||
<Alert type="info" showIcon message={copy.verifiedPhone} className="mb-6"/>
|
||||
<Button type="primary" htmlType="submit" size="large" loading={saving}>{copy.save}</Button>
|
||||
</Form>
|
||||
</>;
|
||||
return (
|
||||
<>
|
||||
<h1 className="text-3xl font-extrabold text-primary">{copy.profile}</h1>
|
||||
<p className="text-secondary mt-3 mb-7">{copy.invoicePending}</p>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={submit}
|
||||
className="rounded-2xl border border-subtle bg-elevated p-5 md:p-7"
|
||||
>
|
||||
<Form.Item name="type" label={copy.customerType} rules={[{required: true}]}>
|
||||
<Radio.Group
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
options={[
|
||||
{
|
||||
label: copy.individual,
|
||||
value: 'individual',
|
||||
},
|
||||
{
|
||||
label: copy.corporate,
|
||||
value: 'corporate',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div className="grid md:grid-cols-2 gap-x-5">
|
||||
<Form.Item name="firstName" label={copy.firstName} rules={[{required: true}]}>
|
||||
<Input size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item name="lastName" label={copy.lastName} rules={[{required: true}]}>
|
||||
<Input size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item name="phone" label={copy.phone}>
|
||||
<Input size="large" dir="ltr" disabled />
|
||||
</Form.Item>
|
||||
<Form.Item name="email" label={copy.email} rules={[{type: 'email'}]}>
|
||||
<Input size="large" dir="ltr" />
|
||||
</Form.Item>
|
||||
{customerType === 'corporate' && (
|
||||
<>
|
||||
<Form.Item name="companyName" label={copy.companyName} rules={[{required: true}]}>
|
||||
<Input size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item name="nationalId" label={copy.nationalId} rules={[{required: true}]}>
|
||||
<Input size="large" dir="ltr" />
|
||||
</Form.Item>
|
||||
<Form.Item name="economicCode" label={copy.economicCode}>
|
||||
<Input size="large" dir="ltr" />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
<Form.Item name="province" label={copy.province} rules={[{required: true}]}>
|
||||
<Input size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item name="city" label={copy.city} rules={[{required: true}]}>
|
||||
<Input size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item name="postalCode" label={copy.postalCode} rules={[{required: true}]}>
|
||||
<Input size="large" dir="ltr" maxLength={10} />
|
||||
</Form.Item>
|
||||
<Form.Item name="preferredCallTime" label={copy.preferredCallTime}>
|
||||
<Input size="large" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item name="address" label={copy.address} rules={[{required: true}]}>
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
<Alert type="info" showIcon message={copy.verifiedPhone} className="mb-6" />
|
||||
<Button type="primary" htmlType="submit" size="large" loading={saving}>
|
||||
{copy.save}
|
||||
</Button>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+103
-21
@@ -169,8 +169,9 @@ html[dir='rtl'] body {
|
||||
height: 190px;
|
||||
border-radius: 999px;
|
||||
border: 2px solid rgba(0, 166, 147, 0.9);
|
||||
box-shadow: 0 0 0 4px rgba(0, 166, 147, 0.15),
|
||||
0 0 40px 6px rgba(0, 166, 147, 0.45);
|
||||
box-shadow:
|
||||
0 0 0 4px rgba(0, 166, 147, 0.15),
|
||||
0 0 40px 6px rgba(0, 166, 147, 0.45);
|
||||
pointer-events: none;
|
||||
transform: translate(-50%, -50%);
|
||||
opacity: 0;
|
||||
@@ -233,32 +234,113 @@ html[dir='rtl'] body {
|
||||
border-bottom: 2px solid #00a693;
|
||||
}
|
||||
|
||||
.invoice-head img { width: 150px; height: auto; }
|
||||
.invoice-head h2 { margin: 0 0 8px; font-size: 26px; }
|
||||
.invoice-head p, .invoice-meta p { margin: 4px 0; }
|
||||
.invoice-meta { margin: 28px 0; }
|
||||
.invoice-meta > div { flex: 1; line-height: 1.9; }
|
||||
.invoice-document table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
.invoice-document th { background: #e9f8f5; }
|
||||
.invoice-document th, .invoice-document td { border: 1px solid #b9ded7; padding: 12px 9px; text-align: start; vertical-align: top; }
|
||||
.invoice-document td small { display: block; color: #55756f; margin-top: 5px; }
|
||||
.invoice-totals { width: 380px; margin-inline-start: auto; margin-top: 24px; }
|
||||
.invoice-totals p { display: flex; justify-content: space-between; gap: 20px; margin: 0; padding: 9px 0; border-bottom: 1px solid #dcebe8; }
|
||||
.invoice-totals .grand { color: #006d61; font-size: 16px; border-bottom: 2px solid #00a693; }
|
||||
.invoice-note { margin-top: 40px; padding: 16px; background: #f2faf8; border-radius: 10px; line-height: 2; }
|
||||
.invoice-head img {
|
||||
width: 150px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.invoice-head h2 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.invoice-head p,
|
||||
.invoice-meta p {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.invoice-meta {
|
||||
margin: 28px 0;
|
||||
}
|
||||
|
||||
.invoice-meta > div {
|
||||
flex: 1;
|
||||
line-height: 1.9;
|
||||
}
|
||||
|
||||
.invoice-document table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.invoice-document th {
|
||||
background: #e9f8f5;
|
||||
}
|
||||
|
||||
.invoice-document th,
|
||||
.invoice-document td {
|
||||
border: 1px solid #b9ded7;
|
||||
padding: 12px 9px;
|
||||
text-align: start;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.invoice-document td small {
|
||||
display: block;
|
||||
color: #55756f;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.invoice-totals {
|
||||
width: 380px;
|
||||
margin-inline-start: auto;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.invoice-totals p {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
margin: 0;
|
||||
padding: 9px 0;
|
||||
border-bottom: 1px solid #dcebe8;
|
||||
}
|
||||
|
||||
.invoice-totals .grand {
|
||||
color: #006d61;
|
||||
font-size: 16px;
|
||||
border-bottom: 2px solid #00a693;
|
||||
}
|
||||
|
||||
.invoice-note {
|
||||
margin-top: 40px;
|
||||
padding: 16px;
|
||||
background: #f2faf8;
|
||||
border-radius: 10px;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body * { visibility: hidden !important; }
|
||||
.invoice-document, .invoice-document * { visibility: visible !important; }
|
||||
.invoice-document { position: absolute; inset: 0; width: 100%; min-height: auto; padding: 20px; }
|
||||
.invoice-actions { display: none !important; }
|
||||
body * {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
.invoice-document,
|
||||
.invoice-document * {
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.invoice-document {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
min-height: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.invoice-actions {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes caretBlink {
|
||||
0%, 49% {
|
||||
0%,
|
||||
49% {
|
||||
opacity: 1;
|
||||
}
|
||||
50%, 100% {
|
||||
50%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
+9
-14
@@ -13,31 +13,26 @@ const vazirmatn = Vazirmatn({
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'روبات مارکت | دستگاههای وندینگ هوشمند',
|
||||
description:
|
||||
'روبات مارکت؛ دستگاههای فروش خودکار هوشمند با مدیریت آنلاین، تحویل نرم و طراحی مدرن.',
|
||||
description: 'روبات مارکت؛ دستگاههای فروش خودکار هوشمند با مدیریت آنلاین، تحویل نرم و طراحی مدرن.',
|
||||
};
|
||||
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: '#00a693',
|
||||
};
|
||||
|
||||
|
||||
export default function RootLayout({children}: {children: ReactNode}) {
|
||||
|
||||
return (
|
||||
<html lang="fa" dir="rtl" className={`${vazirmatn.variable} light`} suppressHydrationWarning>
|
||||
<body className="bg-page text-primary antialiased transition-colors duration-300">
|
||||
<Providers>
|
||||
<Header/>
|
||||
{children}
|
||||
<Footer/>
|
||||
<DevelopmentNotice/>
|
||||
</Providers>
|
||||
</body>
|
||||
<body className="bg-page text-primary antialiased transition-colors duration-300">
|
||||
<Providers>
|
||||
<Header />
|
||||
{children}
|
||||
<Footer />
|
||||
<DevelopmentNotice />
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
+11
-3
@@ -16,7 +16,15 @@ export default function LoginPage() {
|
||||
if (auth.isAuthenticated) router.replace('/dashboard');
|
||||
else if (auth.ready) auth.openLogin(() => router.replace('/dashboard'));
|
||||
}, [auth.isAuthenticated, auth.ready, auth.openLogin, router]);
|
||||
return <main className="max-w-2xl mx-auto min-h-[70vh] px-5 pt-32 pb-20 text-center">
|
||||
<div className="rounded-3xl border border-subtle bg-elevated p-10"><h1 className="text-3xl font-extrabold text-primary">{copy.login}</h1><p className="text-secondary mt-3 mb-7">{copy.protectedDescription}</p><Button type="primary" size="large" onClick={() => auth.openLogin(() => router.replace('/dashboard'))}>{copy.openLogin}</Button></div>
|
||||
</main>;
|
||||
return (
|
||||
<main className="max-w-2xl mx-auto min-h-[70vh] px-5 pt-32 pb-20 text-center">
|
||||
<div className="rounded-3xl border border-subtle bg-elevated p-10">
|
||||
<h1 className="text-3xl font-extrabold text-primary">{copy.login}</h1>
|
||||
<p className="text-secondary mt-3 mb-7">{copy.protectedDescription}</p>
|
||||
<Button type="primary" size="large" onClick={() => auth.openLogin(() => router.replace('/dashboard'))}>
|
||||
{copy.openLogin}
|
||||
</Button>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
+6
-7
@@ -1,18 +1,17 @@
|
||||
import LocationsSection from '@/components/LocationsSection';
|
||||
import ShowcaseSection from "@/components/ShowcaseSection";
|
||||
import ShowcaseSection from '@/components/ShowcaseSection';
|
||||
import FeaturesSection from '@/components/FeaturesSection';
|
||||
import AboutSection from '@/components/AboutSection';
|
||||
import HeroSection from '@/components/HeroSection';
|
||||
|
||||
export default function HomePage() {
|
||||
|
||||
return (
|
||||
<main className="relative bg-page">
|
||||
<HeroSection/>
|
||||
<ShowcaseSection/>
|
||||
<FeaturesSection/>
|
||||
<AboutSection/>
|
||||
<LocationsSection/>
|
||||
<HeroSection />
|
||||
<ShowcaseSection />
|
||||
<FeaturesSection />
|
||||
<AboutSection />
|
||||
<LocationsSection />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {content} from '@/data/content';
|
||||
import ProductDetails from '@/components/ProductDetails';
|
||||
import ProtectedPage from '@/components/auth/ProtectedPage';
|
||||
|
||||
type Props = { params: { id: string } };
|
||||
type Props = {params: {id: string}};
|
||||
|
||||
export function generateStaticParams() {
|
||||
return content.fa.machines.map(machine => ({id: machine.id}));
|
||||
@@ -18,5 +18,9 @@ export function generateMetadata({params}: Props): Metadata {
|
||||
|
||||
export default function ProductPage({params}: Props) {
|
||||
if (!content.fa.machines.some(machine => machine.id === params.id)) notFound();
|
||||
return <ProtectedPage><ProductDetails id={params.id}/></ProtectedPage>;
|
||||
return (
|
||||
<ProtectedPage>
|
||||
<ProductDetails id={params.id} />
|
||||
</ProtectedPage>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,14 @@ export const metadata: Metadata = {title: 'همه محصولات | روبات م
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Suspense fallback={<div className="min-h-screen pt-28 px-6" aria-busy="true"><div className="h-60 rounded-3xl bg-surface animate-pulse"/></div>}>
|
||||
<ProductsPage/>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="min-h-screen pt-28 px-6" aria-busy="true">
|
||||
<div className="h-60 rounded-3xl bg-surface animate-pulse" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ProductsPage />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ function AntdBridge({children}: {children: ReactNode}) {
|
||||
export default function Providers({children}: {children: ReactNode}) {
|
||||
return (
|
||||
<ReduxProvider store={store}>
|
||||
<CartPersistence/>
|
||||
<CartPersistence />
|
||||
<ThemeProvider>
|
||||
<LanguageProvider>
|
||||
<AntdBridge>
|
||||
|
||||
Reference in New Issue
Block a user