chore: format project automatically before starting dev server
This commit is contained in:
@@ -27,7 +27,7 @@ export default function AboutSection() {
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
onClick={() => setExpanded(v => !v)}
|
||||
className="mt-6 inline-flex items-center gap-2 text-brand-400 hover:text-brand-300 text-sm font-semibold transition-colors"
|
||||
>
|
||||
{expanded ? t.about.less : t.about.more}
|
||||
|
||||
+39
-13
@@ -19,13 +19,17 @@ export default function CartPage() {
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-7xl min-h-[65vh] px-5 md:px-8 pt-28 pb-20">
|
||||
<Link href="/products" className="text-brand-400 hover:underline">{copy.continueShopping}</Link>
|
||||
<Link href="/products" className="text-brand-400 hover:underline">
|
||||
{copy.continueShopping}
|
||||
</Link>
|
||||
<h1 className="text-3xl md:text-4xl font-extrabold text-primary mt-6">{t.cart.title}</h1>
|
||||
<p className="text-secondary mt-3 mb-10">{copy.cartDescription}</p>
|
||||
{items.length === 0 ? (
|
||||
<div className="rounded-3xl border border-subtle bg-surface p-10 text-center">
|
||||
<Empty description={t.cart.empty}/>
|
||||
<Link href="/products" className="inline-block mt-6 text-brand-400 hover:underline">{copy.continueShopping}</Link>
|
||||
<Empty description={t.cart.empty} />
|
||||
<Link href="/products" className="inline-block mt-6 text-brand-400 hover:underline">
|
||||
{copy.continueShopping}
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid lg:grid-cols-[minmax(0,1fr),340px] gap-8 items-start">
|
||||
@@ -33,20 +37,40 @@ export default function CartPage() {
|
||||
{items.map(item => {
|
||||
const product = t.machines.find(machine => machine.id === item.id) ?? item;
|
||||
return (
|
||||
<article key={item.id} className="rounded-2xl border border-subtle bg-surface p-5 flex flex-wrap sm:flex-nowrap gap-5 items-center">
|
||||
<article
|
||||
key={item.id}
|
||||
className="rounded-2xl border border-subtle bg-surface p-5 flex flex-wrap sm:flex-nowrap gap-5 items-center"
|
||||
>
|
||||
<Link href={`/products/${item.id}`} className="shrink-0">
|
||||
<img src={product.image} alt={product.name} className="w-24 h-32 object-contain"/>
|
||||
<img
|
||||
src={product.image}
|
||||
alt={product.name}
|
||||
className="w-24 h-32 object-contain"
|
||||
/>
|
||||
</Link>
|
||||
<div className="flex-1 min-w-0">
|
||||
<Link href={`/products/${item.id}`} className="font-bold text-lg text-primary hover:text-brand-400">{product.name}</Link>
|
||||
<p className="text-secondary text-sm mt-2">{copy.unitPrice}: {money(item.priceNumeric)}</p>
|
||||
<Link
|
||||
href={`/products/${item.id}`}
|
||||
className="font-bold text-lg text-primary hover:text-brand-400"
|
||||
>
|
||||
{product.name}
|
||||
</Link>
|
||||
<p className="text-secondary text-sm mt-2">
|
||||
{copy.unitPrice}: {money(item.priceNumeric)}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3 items-center mt-4">
|
||||
<QuantityControl product={product}/>
|
||||
<Button danger type="text" icon={<DeleteOutlined/>}
|
||||
aria-label={`${copy.remove}: ${product.name}`}
|
||||
onClick={() => dispatch(removeFromCart(item.id))}/>
|
||||
<QuantityControl product={product} />
|
||||
<Button
|
||||
danger
|
||||
type="text"
|
||||
icon={<DeleteOutlined />}
|
||||
aria-label={`${copy.remove}: ${product.name}`}
|
||||
onClick={() => dispatch(removeFromCart(item.id))}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-4 text-primary font-semibold">{copy.lineTotal}: {money(item.qty * item.priceNumeric)}</p>
|
||||
<p className="mt-4 text-primary font-semibold">
|
||||
{copy.lineTotal}: {money(item.qty * item.priceNumeric)}
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
@@ -59,7 +83,9 @@ export default function CartPage() {
|
||||
<strong className="text-primary">{money(total)}</strong>
|
||||
</div>
|
||||
<p className="text-secondary text-sm leading-7 my-5">{copy.checkoutUnavailable}</p>
|
||||
<Button type="primary" size="large" disabled block>{t.cart.checkout}</Button>
|
||||
<Button type="primary" size="large" disabled block>
|
||||
{t.cart.checkout}
|
||||
</Button>
|
||||
</aside>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -19,13 +19,25 @@ export default function CartPersistence() {
|
||||
if (Array.isArray(saved)) {
|
||||
const items: CartItem[] = [];
|
||||
for (const entry of saved) {
|
||||
if (!entry || typeof entry !== 'object' || typeof entry.id !== 'string'
|
||||
|| !Number.isSafeInteger(entry.qty) || entry.qty < 1) continue;
|
||||
if (
|
||||
!entry ||
|
||||
typeof entry !== 'object' ||
|
||||
typeof entry.id !== 'string' ||
|
||||
!Number.isSafeInteger(entry.qty) ||
|
||||
entry.qty < 1
|
||||
)
|
||||
continue;
|
||||
const machine = content.fa.machines.find(item => item.id === entry.id);
|
||||
if (!machine || items.some(item => item.id === machine.id)) continue;
|
||||
// Restore identifiers and quantities; use current catalog prices and images.
|
||||
items.push({id: machine.id, name: machine.name, image: machine.image,
|
||||
priceNumeric: machine.priceNumeric, priceLabel: machine.priceLabel, qty: entry.qty});
|
||||
items.push({
|
||||
id: machine.id,
|
||||
name: machine.name,
|
||||
image: machine.image,
|
||||
priceNumeric: machine.priceNumeric,
|
||||
priceLabel: machine.priceLabel,
|
||||
qty: entry.qty,
|
||||
});
|
||||
}
|
||||
store.dispatch(restoreCart(items));
|
||||
}
|
||||
@@ -35,7 +47,10 @@ export default function CartPersistence() {
|
||||
|
||||
return store.subscribe(() => {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(store.getState().cart.items.map(({id, qty}) => ({id, qty}))));
|
||||
localStorage.setItem(
|
||||
STORAGE_KEY,
|
||||
JSON.stringify(store.getState().cart.items.map(({id, qty}) => ({id, qty}))),
|
||||
);
|
||||
} catch {
|
||||
// Keep the in-memory cart usable when storage is full or disabled.
|
||||
}
|
||||
|
||||
@@ -32,9 +32,18 @@ export default function DevelopmentNotice() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} centered title={copy.developmentTitle} onCancel={dismiss}
|
||||
footer={<Button type="primary" onClick={dismiss}>{copy.acknowledge}</Button>}>
|
||||
<ToolOutlined className="text-brand-400 text-3xl my-4"/>
|
||||
<Modal
|
||||
open={open}
|
||||
centered
|
||||
title={copy.developmentTitle}
|
||||
onCancel={dismiss}
|
||||
footer={
|
||||
<Button type="primary" onClick={dismiss}>
|
||||
{copy.acknowledge}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<ToolOutlined className="text-brand-400 text-3xl my-4" />
|
||||
<p className="text-secondary leading-8">{copy.developmentMessage}</p>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -2,43 +2,38 @@
|
||||
|
||||
// Service benefits: warranty, consultation, installments, and packaging.
|
||||
|
||||
import {
|
||||
SafetyCertificateOutlined,
|
||||
CustomerServiceOutlined,
|
||||
CreditCardOutlined,
|
||||
InboxOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useLanguage } from '@/app/context/LanguageContext';
|
||||
import {SafetyCertificateOutlined, CustomerServiceOutlined, CreditCardOutlined, InboxOutlined} from '@ant-design/icons';
|
||||
import {useLanguage} from '@/app/context/LanguageContext';
|
||||
|
||||
const ICONS = {
|
||||
safety: SafetyCertificateOutlined,
|
||||
consulting: CustomerServiceOutlined,
|
||||
installment: CreditCardOutlined,
|
||||
packaging: InboxOutlined,
|
||||
safety: SafetyCertificateOutlined,
|
||||
consulting: CustomerServiceOutlined,
|
||||
installment: CreditCardOutlined,
|
||||
packaging: InboxOutlined,
|
||||
};
|
||||
|
||||
export default function FeaturesSection() {
|
||||
const { t } = useLanguage();
|
||||
const {t} = useLanguage();
|
||||
|
||||
return (
|
||||
<section className="relative px-6 md:px-16 py-16 md:py-20 bg-surface border-t border-subtle">
|
||||
<div className="max-w-6xl mx-auto grid sm:grid-cols-2 lg:grid-cols-4 gap-5">
|
||||
{t.features.map((feature) => {
|
||||
const Icon = ICONS[feature.icon];
|
||||
return (
|
||||
<div
|
||||
key={feature.id}
|
||||
className="group bg-surface border border-subtle rounded-2xl p-6 text-center hover:border-brand-500/60 hover:bg-surface-strong transition-colors"
|
||||
>
|
||||
<div className="mx-auto mb-4 w-14 h-14 rounded-xl bg-brand/15 border border-subtle flex items-center justify-center text-brand-400 text-2xl group-hover:bg-brand/25 transition-colors">
|
||||
{Icon && <Icon />}
|
||||
</div>
|
||||
<h3 className="text-primary font-bold text-base mb-2">{feature.title}</h3>
|
||||
<p className="text-secondary text-sm leading-6">{feature.description}</p>
|
||||
return (
|
||||
<section className="relative px-6 md:px-16 py-16 md:py-20 bg-surface border-t border-subtle">
|
||||
<div className="max-w-6xl mx-auto grid sm:grid-cols-2 lg:grid-cols-4 gap-5">
|
||||
{t.features.map(feature => {
|
||||
const Icon = ICONS[feature.icon];
|
||||
return (
|
||||
<div
|
||||
key={feature.id}
|
||||
className="group bg-surface border border-subtle rounded-2xl p-6 text-center hover:border-brand-500/60 hover:bg-surface-strong transition-colors"
|
||||
>
|
||||
<div className="mx-auto mb-4 w-14 h-14 rounded-xl bg-brand/15 border border-subtle flex items-center justify-center text-brand-400 text-2xl group-hover:bg-brand/25 transition-colors">
|
||||
{Icon && <Icon />}
|
||||
</div>
|
||||
<h3 className="text-primary font-bold text-base mb-2">{feature.title}</h3>
|
||||
<p className="text-secondary text-sm leading-6">{feature.description}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,12 +32,12 @@ export default function FloatingItems({items}: {items: FloatingSnack[]}) {
|
||||
});
|
||||
});
|
||||
|
||||
return () => tweens.forEach((t) => t.kill());
|
||||
return () => tweens.forEach(t => t.kill());
|
||||
}, [items]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="absolute inset-0 pointer-events-none hidden md:block">
|
||||
{items.map((item) => (
|
||||
{items.map(item => (
|
||||
<img
|
||||
key={item.id}
|
||||
data-float-item
|
||||
|
||||
+81
-65
@@ -4,77 +4,93 @@
|
||||
|
||||
import {commerceCopy} from '@/data/commerce';
|
||||
import Link from 'next/link';
|
||||
import { PhoneOutlined, EnvironmentOutlined } from '@ant-design/icons';
|
||||
import { useLanguage } from '@/app/context/LanguageContext';
|
||||
import {PhoneOutlined, EnvironmentOutlined} from '@ant-design/icons';
|
||||
import {useLanguage} from '@/app/context/LanguageContext';
|
||||
|
||||
export default function Footer() {
|
||||
const { t, locale } = useLanguage();
|
||||
const c = t.contactInfo;
|
||||
const {t, locale} = useLanguage();
|
||||
const c = t.contactInfo;
|
||||
|
||||
return (
|
||||
<footer id="contact" className="relative border-t border-subtle bg-elevated">
|
||||
<div className="max-w-6xl mx-auto px-6 md:px-16 py-14 grid md:grid-cols-3 gap-10 text-center md:text-start">
|
||||
<div>
|
||||
<h3 className="text-primary font-bold text-lg mb-3">{t.footer.aboutLabel}</h3>
|
||||
<p className="text-secondary text-sm leading-7">{t.footer.aboutText}</p>
|
||||
<div className="flex flex-col gap-2 mt-4">
|
||||
{c.socialLinks.map((link) => (
|
||||
<a
|
||||
key={link.id}
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-brand-400 hover:text-brand-300 text-sm transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<footer id="contact" className="relative border-t border-subtle bg-elevated">
|
||||
<div className="max-w-6xl mx-auto px-6 md:px-16 py-14 grid md:grid-cols-3 gap-10 text-center md:text-start">
|
||||
<div>
|
||||
<h3 className="text-primary font-bold text-lg mb-3">{t.footer.aboutLabel}</h3>
|
||||
<p className="text-secondary text-sm leading-7">{t.footer.aboutText}</p>
|
||||
<div className="flex flex-col gap-2 mt-4">
|
||||
{c.socialLinks.map(link => (
|
||||
<a
|
||||
key={link.id}
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-brand-400 hover:text-brand-300 text-sm transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-primary font-bold text-lg mb-3">{t.footer.quickLinks}</h3>
|
||||
<ul className="flex flex-col gap-2 text-sm text-secondary">
|
||||
<li><Link href="/#hero" className="hover:text-brand-400">{t.nav.home}</Link></li>
|
||||
<li><Link href="/products" className="hover:text-brand-400">{commerceCopy[locale].allProducts}</Link></li>
|
||||
<li><Link href="/#about" className="hover:text-brand-400">{t.nav.about}</Link></li>
|
||||
<li><Link href="/#locations" className="hover:text-brand-400">{t.nav.locations}</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-primary font-bold text-lg mb-3">{t.footer.quickLinks}</h3>
|
||||
<ul className="flex flex-col gap-2 text-sm text-secondary">
|
||||
<li>
|
||||
<Link href="/#hero" className="hover:text-brand-400">
|
||||
{t.nav.home}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/products" className="hover:text-brand-400">
|
||||
{commerceCopy[locale].allProducts}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/#about" className="hover:text-brand-400">
|
||||
{t.nav.about}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/#locations" className="hover:text-brand-400">
|
||||
{t.nav.locations}
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-primary font-bold text-lg mb-3">{t.footer.contactTitle}</h3>
|
||||
<div className="flex flex-col gap-4 text-sm text-secondary">
|
||||
<a
|
||||
href={`tel:${c.orderPhoneHref}`}
|
||||
className="flex items-center justify-center md:justify-start gap-2 hover:text-brand-400 transition-colors"
|
||||
dir="ltr"
|
||||
>
|
||||
<PhoneOutlined />
|
||||
<span>{c.orderPhone}</span>
|
||||
</a>
|
||||
<div>
|
||||
<h3 className="text-primary font-bold text-lg mb-3">{t.footer.contactTitle}</h3>
|
||||
<div className="flex flex-col gap-4 text-sm text-secondary">
|
||||
<a
|
||||
href={`tel:${c.orderPhoneHref}`}
|
||||
className="flex items-center justify-center md:justify-start gap-2 hover:text-brand-400 transition-colors"
|
||||
dir="ltr"
|
||||
>
|
||||
<PhoneOutlined />
|
||||
<span>{c.orderPhone}</span>
|
||||
</a>
|
||||
|
||||
<div className="flex items-start justify-center md:justify-start gap-2">
|
||||
<EnvironmentOutlined className="mt-0.5 shrink-0" />
|
||||
<div className="text-start">
|
||||
<p className="text-primary-90 font-semibold mb-1">{t.footer.salesLabel}</p>
|
||||
<p className="leading-7">{c.salesOfficeAddress}</p>
|
||||
</div>
|
||||
<div className="flex items-start justify-center md:justify-start gap-2">
|
||||
<EnvironmentOutlined className="mt-0.5 shrink-0" />
|
||||
<div className="text-start">
|
||||
<p className="text-primary-90 font-semibold mb-1">{t.footer.salesLabel}</p>
|
||||
<p className="leading-7">{c.salesOfficeAddress}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start justify-center md:justify-start gap-2">
|
||||
<EnvironmentOutlined className="mt-0.5 shrink-0" />
|
||||
<div className="text-start">
|
||||
<p className="text-primary-90 font-semibold mb-1">{t.footer.factoryLabel}</p>
|
||||
<p className="leading-7">{c.factoryAddress}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start justify-center md:justify-start gap-2">
|
||||
<EnvironmentOutlined className="mt-0.5 shrink-0" />
|
||||
<div className="text-start">
|
||||
<p className="text-primary-90 font-semibold mb-1">{t.footer.factoryLabel}</p>
|
||||
<p className="leading-7">{c.factoryAddress}</p>
|
||||
</div>
|
||||
<div className="border-t border-subtle py-5 text-center text-xs text-secondary">
|
||||
© {new Date().getFullYear()} {t.footer.aboutLabel}. {t.footer.rights}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-subtle py-5 text-center text-xs text-secondary">
|
||||
© {new Date().getFullYear()} {t.footer.aboutLabel}. {t.footer.rights}
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
+48
-23
@@ -44,24 +44,23 @@ export default function Header() {
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
console.log(
|
||||
"%c👋 Hamed Abdollahzade · Frontend Developer",
|
||||
"font-size:14px;font-weight:600;"
|
||||
);
|
||||
console.log('%c👋 Hamed Abdollahzade · Frontend Developer', 'font-size:14px;font-weight:600;');
|
||||
|
||||
return (
|
||||
<header
|
||||
className={`fixed top-0 inset-x-0 z-50 transition-all duration-300 ${
|
||||
scrolled || pathname !== '/' ? 'bg-page-85 backdrop-blur-md shadow-[0_1px_0_0_rgba(0,166,147,0.25)]' : 'bg-transparent'
|
||||
scrolled || pathname !== '/'
|
||||
? 'bg-page-85 backdrop-blur-md shadow-[0_1px_0_0_rgba(0,166,147,0.25)]'
|
||||
: 'bg-transparent'
|
||||
}`}
|
||||
>
|
||||
<div className="mx-auto max-w-7xl px-5 md:px-8 h-20 flex items-center justify-between gap-4">
|
||||
<Link href="/#hero" onClick={() => setMobileOpen(false)} className="flex items-center gap-2 shrink-0">
|
||||
<Image src="/images/icons/logo.png" alt="روبات مارکت" width={150} height={42} priority/>
|
||||
<Image src="/images/icons/logo.png" alt="روبات مارکت" width={150} height={42} priority />
|
||||
</Link>
|
||||
|
||||
<nav className="hidden xl:flex items-center gap-5 2xl:gap-7">
|
||||
{navLinks.map((link) => (
|
||||
{navLinks.map(link => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
@@ -74,37 +73,47 @@ export default function Header() {
|
||||
|
||||
<div className="flex items-center gap-2 md:gap-3">
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
<LanguageToggle/>
|
||||
<ThemeToggle/>
|
||||
<LanguageToggle />
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
{auth.isAuthenticated ? (
|
||||
<Button className="hidden sm:inline-flex" icon={<UserOutlined/>} href="/dashboard">{accountLabel}</Button>
|
||||
<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>
|
||||
<Button
|
||||
className="hidden sm:inline-flex"
|
||||
icon={<UserOutlined />}
|
||||
onClick={() => auth.openLogin()}
|
||||
>
|
||||
{account.login}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Badge count={cartCount} size="small" offset={[-2, 2]}>
|
||||
<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/>
|
||||
<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 />
|
||||
</ProtectedLink>
|
||||
</Badge>
|
||||
|
||||
<button
|
||||
className="xl:hidden text-brand-400 text-2xl"
|
||||
onClick={() => setMobileOpen((v) => !v)}
|
||||
onClick={() => setMobileOpen(v => !v)}
|
||||
aria-label="menu"
|
||||
>
|
||||
{mobileOpen ? <CloseOutlined/> : <MenuOutlined/>}
|
||||
{mobileOpen ? <CloseOutlined /> : <MenuOutlined />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{mobileOpen && (
|
||||
<nav
|
||||
className="xl:hidden bg-elevated-95 backdrop-blur-md border-t border-subtle px-5 py-4 flex flex-col gap-4 max-h-[calc(100vh-5rem)] overflow-y-auto">
|
||||
{navLinks.map((link) => (
|
||||
<nav className="xl:hidden bg-elevated-95 backdrop-blur-md border-t border-subtle px-5 py-4 flex flex-col gap-4 max-h-[calc(100vh-5rem)] overflow-y-auto">
|
||||
{navLinks.map(link => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
@@ -115,12 +124,28 @@ export default function Header() {
|
||||
</Link>
|
||||
))}
|
||||
<div className="flex items-center gap-3 pt-2 border-t border-subtle">
|
||||
<LanguageToggle/>
|
||||
<ThemeToggle/>
|
||||
<LanguageToggle />
|
||||
<ThemeToggle />
|
||||
{auth.isAuthenticated ? (
|
||||
<Button icon={<UserOutlined/>} className="flex-1" href="/dashboard" onClick={() => setMobileOpen(false)}>{accountLabel}</Button>
|
||||
<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>
|
||||
<Button
|
||||
icon={<UserOutlined />}
|
||||
className="flex-1"
|
||||
onClick={() => {
|
||||
setMobileOpen(false);
|
||||
auth.openLogin();
|
||||
}}
|
||||
>
|
||||
{account.login}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
+20
-14
@@ -17,11 +17,11 @@ export default function HeroSection() {
|
||||
|
||||
// Wrap around after the last slide.
|
||||
const goToNext = useCallback(() => {
|
||||
setActiveIndex((prev) => (prev + 1) % slides.length);
|
||||
setActiveIndex(prev => (prev + 1) % slides.length);
|
||||
}, [slides.length]);
|
||||
|
||||
const goToPrev = useCallback(() => {
|
||||
setActiveIndex((prev) => (prev - 1 + slides.length) % slides.length);
|
||||
setActiveIndex(prev => (prev - 1 + slides.length) % slides.length);
|
||||
}, [slides.length]);
|
||||
|
||||
// Restart the autoplay timer after manual navigation.
|
||||
@@ -94,26 +94,32 @@ export default function HeroSection() {
|
||||
{/* ------------------------------------------------------------------ */}
|
||||
{/* Active slide headline, description, and call to action. */}
|
||||
{/* ------------------------------------------------------------------ */}
|
||||
<div
|
||||
className="relative z-10 h-full flex flex-col justify-end md:justify-end px-6 md:px-16 pb-28 md:pb-72">
|
||||
<div className="relative z-10 h-full flex flex-col justify-end md:justify-end px-6 md:px-16 pb-28 md:pb-72">
|
||||
<div className="max-w-2xl">
|
||||
<span className="text-brand-400 font-semibold tracking-widest text-sm uppercase">
|
||||
{slide.kicker}
|
||||
</span>
|
||||
<span className="text-brand-400 font-semibold tracking-widest text-sm uppercase">
|
||||
{slide.kicker}
|
||||
</span>
|
||||
<h1 className="mt-3 text-3xl sm:text-4xl md:text-6xl font-extrabold leading-tight text-white drop-shadow-[0_4px_12px_rgba(0,0,0,0.9)]">
|
||||
{slide.titleLines.map((line, i) => (
|
||||
<span key={i} className="block">
|
||||
{line}
|
||||
</span>
|
||||
{line}
|
||||
</span>
|
||||
))}
|
||||
</h1>
|
||||
<p className="mt-5 text-white/90 text-base md:text-lg max-w-xl drop-shadow-lg">{slide.sub}</p>
|
||||
<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">
|
||||
<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">
|
||||
<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>
|
||||
@@ -128,14 +134,14 @@ export default function HeroSection() {
|
||||
aria-label="previous slide"
|
||||
className="hidden sm:flex absolute top-1/2 -translate-y-1/2 start-4 z-20 w-11 h-11 rounded-full bg-page-70 border border-subtle items-center justify-center text-primary hover:bg-brand hover:text-ink-950 transition-colors"
|
||||
>
|
||||
<RightOutlined/>
|
||||
<RightOutlined />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleManualNav('next')}
|
||||
aria-label="next slide"
|
||||
className="hidden sm:flex absolute top-1/2 -translate-y-1/2 end-4 z-20 w-11 h-11 rounded-full bg-page-70 border border-subtle items-center justify-center text-primary hover:bg-brand hover:text-ink-950 transition-colors"
|
||||
>
|
||||
<LeftOutlined/>
|
||||
<LeftOutlined />
|
||||
</button>
|
||||
|
||||
{/* Jump directly to a slide using the indicators. */}
|
||||
|
||||
@@ -27,9 +27,7 @@ export default function HorizontalShowcase() {
|
||||
|
||||
if (!track || !section) return;
|
||||
|
||||
const scrollDistance = () => (
|
||||
Math.max(track.scrollWidth - window.innerWidth, 0)
|
||||
);
|
||||
const scrollDistance = () => Math.max(track.scrollWidth - window.innerWidth, 0);
|
||||
|
||||
let animationFrameId: number | null = null;
|
||||
let latestProgress = 0;
|
||||
@@ -57,7 +55,7 @@ export default function HorizontalShowcase() {
|
||||
scrub: 1,
|
||||
pin: true,
|
||||
invalidateOnRefresh: true,
|
||||
onUpdate: (self) => syncVideo(self.progress),
|
||||
onUpdate: self => syncVideo(self.progress),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -97,18 +95,22 @@ export default function HorizontalShowcase() {
|
||||
<div className="relative z-20 h-40 pt-20 bg-page-85 backdrop-blur-md">
|
||||
<div className="max-w-7xl h-full mx-auto px-8 flex justify-between items-center gap-4">
|
||||
<h2 className="text-2xl font-bold text-primary">{copy.featuredProducts}</h2>
|
||||
<Link href="/products" className="text-brand-400 hover:underline">{copy.allProducts}</Link>
|
||||
<Link href="/products" className="text-brand-400 hover:underline">
|
||||
{copy.allProducts}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref={trackRef}
|
||||
className="horizontal-track relative z-10 [&_.horizontal-panel]:h-full [&_.spotlight-stage]:max-h-[calc(100vh-12rem)]"
|
||||
style={{height: 'calc(100vh - 10rem)'}} dir="ltr">
|
||||
{featuredMachines(t.machines).map((machine) => (
|
||||
<VendingCard key={machine.id} machine={machine}/>
|
||||
<div
|
||||
ref={trackRef}
|
||||
className="horizontal-track relative z-10 [&_.horizontal-panel]:h-full [&_.spotlight-stage]:max-h-[calc(100vh-12rem)]"
|
||||
style={{height: 'calc(100vh - 10rem)'}}
|
||||
dir="ltr"
|
||||
>
|
||||
{featuredMachines(t.machines).map(machine => (
|
||||
<VendingCard key={machine.id} machine={machine} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
|
||||
// Switch between Persian and English.
|
||||
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { useLanguage } from '@/app/context/LanguageContext';
|
||||
import {Button, Tooltip} from 'antd';
|
||||
import {useLanguage} from '@/app/context/LanguageContext';
|
||||
|
||||
export default function LanguageToggle() {
|
||||
const { locale, toggleLocale } = useLanguage();
|
||||
const {locale, toggleLocale} = useLanguage();
|
||||
|
||||
return (
|
||||
<Tooltip title={locale === 'fa' ? 'Switch to English' : 'تغییر به فارسی'}>
|
||||
<Button shape="circle" size="large" onClick={toggleLocale} aria-label="toggle language">
|
||||
{locale === 'fa' ? 'EN' : 'فا'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
return (
|
||||
<Tooltip title={locale === 'fa' ? 'Switch to English' : 'تغییر به فارسی'}>
|
||||
<Button shape="circle" size="large" onClick={toggleLocale} aria-label="toggle language">
|
||||
{locale === 'fa' ? 'EN' : 'فا'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,52 +3,52 @@
|
||||
// Suggested installation locations for vending machines.
|
||||
|
||||
import {
|
||||
HomeOutlined,
|
||||
ReadOutlined,
|
||||
ShopOutlined,
|
||||
MedicineBoxOutlined,
|
||||
ClockCircleOutlined,
|
||||
AppstoreOutlined,
|
||||
HomeOutlined,
|
||||
ReadOutlined,
|
||||
ShopOutlined,
|
||||
MedicineBoxOutlined,
|
||||
ClockCircleOutlined,
|
||||
AppstoreOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useLanguage } from '@/app/context/LanguageContext';
|
||||
import {useLanguage} from '@/app/context/LanguageContext';
|
||||
|
||||
const ICONS = {
|
||||
hotel: HomeOutlined,
|
||||
school: ReadOutlined,
|
||||
pharmacy: ShopOutlined,
|
||||
hospital: MedicineBoxOutlined,
|
||||
night: ClockCircleOutlined,
|
||||
specialty: AppstoreOutlined,
|
||||
hotel: HomeOutlined,
|
||||
school: ReadOutlined,
|
||||
pharmacy: ShopOutlined,
|
||||
hospital: MedicineBoxOutlined,
|
||||
night: ClockCircleOutlined,
|
||||
specialty: AppstoreOutlined,
|
||||
};
|
||||
|
||||
export default function LocationsSection() {
|
||||
const { t } = useLanguage();
|
||||
const {t} = useLanguage();
|
||||
|
||||
return (
|
||||
<section id="locations" className="relative px-6 md:px-16 py-20 md:py-28 bg-surface border-t border-subtle">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-2xl md:text-4xl font-extrabold brand-gradient-text">{t.locations.title}</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{t.locations.items.map((loc) => {
|
||||
const Icon = ICONS[loc.icon];
|
||||
return (
|
||||
<div
|
||||
key={loc.id}
|
||||
className="bg-page-60 border border-subtle rounded-2xl p-6 hover:border-brand-500/60 transition-colors"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-xl bg-brand/15 border border-subtle flex items-center justify-center text-brand-400 text-xl mb-4">
|
||||
{Icon && <Icon />}
|
||||
return (
|
||||
<section id="locations" className="relative px-6 md:px-16 py-20 md:py-28 bg-surface border-t border-subtle">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-2xl md:text-4xl font-extrabold brand-gradient-text">{t.locations.title}</h2>
|
||||
</div>
|
||||
<h3 className="text-primary font-bold text-base mb-2">{loc.title}</h3>
|
||||
<p className="text-secondary text-sm leading-7">{loc.description}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{t.locations.items.map(loc => {
|
||||
const Icon = ICONS[loc.icon];
|
||||
return (
|
||||
<div
|
||||
key={loc.id}
|
||||
className="bg-page-60 border border-subtle rounded-2xl p-6 hover:border-brand-500/60 transition-colors"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-xl bg-brand/15 border border-subtle flex items-center justify-center text-brand-400 text-xl mb-4">
|
||||
{Icon && <Icon />}
|
||||
</div>
|
||||
<h3 className="text-primary font-bold text-base mb-2">{loc.title}</h3>
|
||||
<p className="text-secondary text-sm leading-7">{loc.description}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,9 +13,12 @@ export default function MobileShowcase() {
|
||||
<section id="showcase" className="relative w-full py-8 px-4" dir={dir}>
|
||||
<div className="flex gap-4 overflow-x-auto snap-x snap-mandatory pb-4">
|
||||
{featuredMachines(t.machines).map(machine => (
|
||||
<article key={machine.id} className="w-[90%] shrink-0 snap-start bg-surface border border-subtle rounded-2xl p-4">
|
||||
<article
|
||||
key={machine.id}
|
||||
className="w-[90%] shrink-0 snap-start bg-surface border border-subtle rounded-2xl p-4"
|
||||
>
|
||||
<Link href={`/products/${machine.id}`} className="flex gap-4 items-center">
|
||||
<img src={machine.image} alt={machine.name} className="w-24 h-28 object-contain shrink-0"/>
|
||||
<img src={machine.image} alt={machine.name} className="w-24 h-28 object-contain shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<span className="text-brand-400 text-xs font-semibold">{machine.shortLabel}</span>
|
||||
<h3 className="text-lg font-bold text-primary mt-1">{machine.name}</h3>
|
||||
@@ -24,7 +27,7 @@ export default function MobileShowcase() {
|
||||
</Link>
|
||||
<p className="text-base font-bold text-primary my-4">{machine.priceLabel}</p>
|
||||
<div className="flex flex-wrap justify-between items-center gap-3">
|
||||
<QuantityControl product={machine}/>
|
||||
<QuantityControl product={machine} />
|
||||
<Link href={`/products/${machine.id}`} className="text-sm text-brand-400 hover:underline">
|
||||
{commerceCopy[locale].details}
|
||||
</Link>
|
||||
|
||||
@@ -17,40 +17,67 @@ export default function ProductDetails({id}: {id: string}) {
|
||||
return (
|
||||
<main className="max-w-7xl mx-auto px-5 md:px-8 pt-28 pb-20">
|
||||
<nav aria-label={copy.details} className="text-sm text-secondary flex flex-wrap gap-3 mb-8">
|
||||
<Link href="/" className="hover:text-brand-400">{t.nav.home}</Link>
|
||||
<Link href="/" className="hover:text-brand-400">
|
||||
{t.nav.home}
|
||||
</Link>
|
||||
<span aria-hidden="true">/</span>
|
||||
<Link href="/products" className="hover:text-brand-400">{t.nav.showcase}</Link>
|
||||
<span aria-hidden="true">/</span><span>{machine.shortLabel}</span>
|
||||
<Link href="/products" className="hover:text-brand-400">
|
||||
{t.nav.showcase}
|
||||
</Link>
|
||||
<span aria-hidden="true">/</span>
|
||||
<span>{machine.shortLabel}</span>
|
||||
</nav>
|
||||
<div className="grid md:grid-cols-2 gap-10 lg:gap-16 items-start">
|
||||
<section aria-label={copy.gallery} className="rounded-3xl border border-subtle bg-surface p-6">
|
||||
<img src={alternate ? machine.imageAlt : machine.image} alt={`${machine.name} — ${alternate ? copy.alternateImage : copy.baseImage}`}
|
||||
className="w-full h-72 md:h-[450px] object-contain"/>
|
||||
<img
|
||||
src={alternate ? machine.imageAlt : machine.image}
|
||||
alt={`${machine.name} — ${alternate ? copy.alternateImage : copy.baseImage}`}
|
||||
className="w-full h-72 md:h-[450px] object-contain"
|
||||
/>
|
||||
<div className="flex justify-center gap-3 mt-6">
|
||||
{[false, true].map(value => (
|
||||
<button key={String(value)} onClick={() => setAlternate(value)} aria-pressed={alternate === value}
|
||||
aria-label={value ? copy.alternateImage : copy.baseImage}
|
||||
className={`rounded-xl border p-2 ${alternate === value ? 'border-brand bg-brand/10' : 'border-subtle'}`}>
|
||||
<img src={value ? machine.imageAlt : machine.image} alt="" className="h-20 w-20 object-contain"/>
|
||||
<button
|
||||
key={String(value)}
|
||||
onClick={() => setAlternate(value)}
|
||||
aria-pressed={alternate === value}
|
||||
aria-label={value ? copy.alternateImage : copy.baseImage}
|
||||
className={`rounded-xl border p-2 ${alternate === value ? 'border-brand bg-brand/10' : 'border-subtle'}`}
|
||||
>
|
||||
<img
|
||||
src={value ? machine.imageAlt : machine.image}
|
||||
alt=""
|
||||
className="h-20 w-20 object-contain"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<p className="text-brand-400 font-semibold">{machine.shortLabel}</p>
|
||||
<h1 className="text-3xl lg:text-4xl font-extrabold text-primary mt-3 leading-relaxed">{machine.name}</h1>
|
||||
<h1 className="text-3xl lg:text-4xl font-extrabold text-primary mt-3 leading-relaxed">
|
||||
{machine.name}
|
||||
</h1>
|
||||
<p className="text-secondary mt-5 leading-8">{machine.description}</p>
|
||||
<p className="text-sm text-secondary mt-4">{copy.model}: <span dir="ltr">{machine.id.toUpperCase()}</span></p>
|
||||
<p className="text-sm text-secondary mt-4">
|
||||
{copy.model}: <span dir="ltr">{machine.id.toUpperCase()}</span>
|
||||
</p>
|
||||
<h2 className="font-bold text-primary text-xl mt-8 mb-4">{copy.specifications}</h2>
|
||||
<ul className="space-y-3">
|
||||
{machine.highlights.map(feature => <li key={feature} className="flex gap-3 text-secondary"><CheckCircleOutlined className="text-brand-400"/>{feature}</li>)}
|
||||
{machine.highlights.map(feature => (
|
||||
<li key={feature} className="flex gap-3 text-secondary">
|
||||
<CheckCircleOutlined className="text-brand-400" />
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="border-t border-subtle mt-8 pt-6">
|
||||
<p className="text-sm text-secondary">{copy.unitPrice}</p>
|
||||
<p className="text-2xl font-bold text-primary mt-2">{machine.priceLabel}</p>
|
||||
<div className="mt-5 flex flex-wrap gap-4 items-center">
|
||||
<QuantityControl product={machine}/>
|
||||
<Link href="/cart" className="text-brand-400 font-semibold hover:underline">{copy.cartLink}</Link>
|
||||
<QuantityControl product={machine} />
|
||||
<Link href="/cart" className="text-brand-400 font-semibold hover:underline">
|
||||
{copy.cartLink}
|
||||
</Link>
|
||||
</div>
|
||||
<p className="text-secondary text-sm leading-7 mt-5">{copy.checkoutUnavailable}</p>
|
||||
</div>
|
||||
|
||||
+42
-12
@@ -32,15 +32,29 @@ export default function ProductsPage() {
|
||||
<p className="text-secondary mt-4 leading-7">{copy.productsDescription}</p>
|
||||
<div className="mt-8 mb-8 rounded-2xl border border-subtle bg-surface p-5 flex flex-wrap items-end gap-4">
|
||||
<div className="flex flex-col gap-2 w-full sm:w-72">
|
||||
<label htmlFor="product-category" className="text-sm font-semibold text-primary">{copy.category}</label>
|
||||
<select id="product-category" value={category} onChange={event => setCategory(event.target.value)}
|
||||
className="w-full border border-subtle bg-elevated text-primary rounded-xl px-4 py-3 focus:outline-brand">
|
||||
<label htmlFor="product-category" className="text-sm font-semibold text-primary">
|
||||
{copy.category}
|
||||
</label>
|
||||
<select
|
||||
id="product-category"
|
||||
value={category}
|
||||
onChange={event => setCategory(event.target.value)}
|
||||
className="w-full border border-subtle bg-elevated text-primary rounded-xl px-4 py-3 focus:outline-brand"
|
||||
>
|
||||
<option value="">{copy.allCategories}</option>
|
||||
{category && !knownCategory && <option value={category}>{copy.unknownCategory}</option>}
|
||||
{productCategories.map(item => <option key={item.id} value={item.id}>{item.label[locale]}</option>)}
|
||||
{productCategories.map(item => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.label[locale]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{category && <button onClick={() => setCategory('')} className="text-brand-400 py-3 hover:underline">{copy.clearFilters}</button>}
|
||||
{category && (
|
||||
<button onClick={() => setCategory('')} className="text-brand-400 py-3 hover:underline">
|
||||
{copy.clearFilters}
|
||||
</button>
|
||||
)}
|
||||
<p aria-live="polite" aria-atomic="true" className="text-secondary text-sm py-3 sm:ms-auto">
|
||||
{products.length.toLocaleString(locale === 'fa' ? 'fa-IR' : 'en-US')} {copy.results}
|
||||
</p>
|
||||
@@ -48,22 +62,38 @@ export default function ProductsPage() {
|
||||
{products.length ? (
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{products.map(machine => (
|
||||
<article key={machine.id} className="flex flex-col rounded-3xl border border-subtle bg-surface p-5">
|
||||
<article
|
||||
key={machine.id}
|
||||
className="flex flex-col rounded-3xl border border-subtle bg-surface p-5"
|
||||
>
|
||||
<Link href={`/products/${machine.id}`} className="rounded-2xl bg-page p-5">
|
||||
<img src={machine.image} alt={machine.name} className="w-full h-60 object-contain"/>
|
||||
<img src={machine.image} alt={machine.name} className="w-full h-60 object-contain" />
|
||||
</Link>
|
||||
<span className="text-brand-400 text-xs mt-5">{productCategories.find(item => item.id === machine.category)?.label[locale]}</span>
|
||||
<h2 className="text-xl font-bold text-primary mt-2"><Link href={`/products/${machine.id}`}>{machine.name}</Link></h2>
|
||||
<span className="text-brand-400 text-xs mt-5">
|
||||
{productCategories.find(item => item.id === machine.category)?.label[locale]}
|
||||
</span>
|
||||
<h2 className="text-xl font-bold text-primary mt-2">
|
||||
<Link href={`/products/${machine.id}`}>{machine.name}</Link>
|
||||
</h2>
|
||||
<p className="text-secondary text-sm leading-7 mt-3 mb-5">{machine.description}</p>
|
||||
<p className="text-primary text-lg font-bold mt-auto mb-4">{machine.priceLabel}</p>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<QuantityControl product={machine}/>
|
||||
<Link href={`/products/${machine.id}`} className="text-brand-400 text-sm hover:underline">{copy.details}</Link>
|
||||
<QuantityControl product={machine} />
|
||||
<Link
|
||||
href={`/products/${machine.id}`}
|
||||
className="text-brand-400 text-sm hover:underline"
|
||||
>
|
||||
{copy.details}
|
||||
</Link>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
) : <div className="text-center text-secondary border border-subtle rounded-2xl p-12">{copy.noProducts}</div>}
|
||||
) : (
|
||||
<div className="text-center text-secondary border border-subtle rounded-2xl p-12">
|
||||
{copy.noProducts}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,21 +17,40 @@ export default function QuantityControl({product}: {product: CartProduct}) {
|
||||
const quantity = useAppSelector(state => state.cart.items.find(item => item.id === product.id)?.qty ?? 0);
|
||||
|
||||
return (
|
||||
<div role="group" aria-label={`${copy.quantity}: ${product.name}`}
|
||||
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={() => auth.isAuthenticated ? dispatch(decrementQty(product.id)) : auth.openLogin()}/>
|
||||
<div
|
||||
role="group"
|
||||
aria-label={`${copy.quantity}: ${product.name}`}
|
||||
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={() => (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={() => {
|
||||
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);
|
||||
}}/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
aria-label={`${copy.increase}: ${product.name}`}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,9 +15,7 @@ export default function ShowcaseSection() {
|
||||
const [isMobile, setIsMobile] = useState<boolean | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia(
|
||||
`(max-width: ${MOBILE_BREAKPOINT - 1}px)`
|
||||
);
|
||||
const mediaQuery = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||
|
||||
const handleChange = (event: MediaQueryListEvent) => setIsMobile(event.matches);
|
||||
|
||||
@@ -28,15 +26,17 @@ export default function ShowcaseSection() {
|
||||
}, []);
|
||||
|
||||
if (isMobile === null) return null;
|
||||
if (!isMobile) return <HorizontalShowcase/>;
|
||||
if (!isMobile) return <HorizontalShowcase />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="max-w-7xl mx-auto px-5 md:px-8 py-8 flex flex-wrap justify-between items-center gap-4">
|
||||
<h2 className="text-2xl font-bold text-primary">{copy.featuredProducts}</h2>
|
||||
<Link href="/products" className="text-brand-400 hover:underline">{copy.allProducts}</Link>
|
||||
<Link href="/products" className="text-brand-400 hover:underline">
|
||||
{copy.allProducts}
|
||||
</Link>
|
||||
</div>
|
||||
<MobileShowcase/>
|
||||
<MobileShowcase />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+15
-16
@@ -1,22 +1,21 @@
|
||||
'use client';
|
||||
|
||||
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { SunOutlined, MoonOutlined } from '@ant-design/icons';
|
||||
import { useTheme } from '@/app/context/ThemeContext';
|
||||
import {Button, Tooltip} from 'antd';
|
||||
import {SunOutlined, MoonOutlined} from '@ant-design/icons';
|
||||
import {useTheme} from '@/app/context/ThemeContext';
|
||||
|
||||
export default function ThemeToggle() {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const {theme, toggleTheme} = useTheme();
|
||||
|
||||
return (
|
||||
<Tooltip title={theme === 'dark' ? 'Light mode' : 'Dark mode'}>
|
||||
<Button
|
||||
shape="circle"
|
||||
size="large"
|
||||
onClick={toggleTheme}
|
||||
aria-label="toggle theme"
|
||||
icon={theme === 'dark' ? <SunOutlined /> : <MoonOutlined />}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
return (
|
||||
<Tooltip title={theme === 'dark' ? 'Light mode' : 'Dark mode'}>
|
||||
<Button
|
||||
shape="circle"
|
||||
size="large"
|
||||
onClick={toggleTheme}
|
||||
aria-label="toggle theme"
|
||||
icon={theme === 'dark' ? <SunOutlined /> : <MoonOutlined />}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
+17
-15
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
|
||||
import type {Machine} from '@/types/domain';
|
||||
import type {MouseEvent} from 'react';
|
||||
import {useRef} from 'react';
|
||||
@@ -45,29 +44,27 @@ export default function VendingCard({machine}: {machine: Machine}) {
|
||||
onMouseLeave={handleMouseLeave}
|
||||
className="spotlight-stage relative h-48 md:h-[500px] flex items-center justify-center"
|
||||
>
|
||||
<FloatingItems items={floatingSnacks}/>
|
||||
<FloatingItems items={floatingSnacks} />
|
||||
|
||||
<img src={machine.image} alt={machine.name} className="base-layer z-10"/>
|
||||
<img src={machine.image} alt={machine.name} className="base-layer z-10" />
|
||||
<img
|
||||
ref={revealRef}
|
||||
src={machine.imageAlt}
|
||||
alt={`${machine.name} - ${t.machineActions.altVariant}`}
|
||||
className="reveal-layer z-20"
|
||||
/>
|
||||
<div ref={lensRef} className="spotlight-lens"/>
|
||||
<div ref={lensRef} className="spotlight-lens" />
|
||||
|
||||
<span
|
||||
className="absolute hidden md:block top-3 z-30 text-[11px] px-3 py-1 rounded-full bg-brand/15 border border-subtle text-brand-400 start-3">
|
||||
{t.machineActions.hoverHint}
|
||||
</span>
|
||||
<span className="absolute hidden md:block top-3 z-30 text-[11px] px-3 py-1 rounded-full bg-brand/15 border border-subtle text-brand-400 start-3">
|
||||
{t.machineActions.hoverHint}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col justify-center items-center gap-5 ">
|
||||
<span className="text-brand-400 text-sm font-semibold tracking-wide">{machine.shortLabel}</span>
|
||||
<h3 className="text-2xl md:text-4xl font-extrabold text-primary">{machine.name}</h3>
|
||||
|
||||
<div
|
||||
className="grid grid-cols-[auto,1fr] gap-4 items-start bg-surface border border-subtle rounded-2xl p-4">
|
||||
<div className="grid grid-cols-[auto,1fr] gap-4 items-start bg-surface border border-subtle rounded-2xl p-4">
|
||||
<img
|
||||
src={machine.gif}
|
||||
alt={t.machineActions.gifAlt(machine.name)}
|
||||
@@ -77,9 +74,11 @@ export default function VendingCard({machine}: {machine: Machine}) {
|
||||
</div>
|
||||
|
||||
<ul className="flex flex-wrap gap-2">
|
||||
{machine.highlights.map((h) => (
|
||||
<li key={h}
|
||||
className="text-xs md:text-sm text-brand-400 bg-brand/10 border border-subtle rounded-full px-3 py-1">
|
||||
{machine.highlights.map(h => (
|
||||
<li
|
||||
key={h}
|
||||
className="text-xs md:text-sm text-brand-400 bg-brand/10 border border-subtle rounded-full px-3 py-1"
|
||||
>
|
||||
{h}
|
||||
</li>
|
||||
))}
|
||||
@@ -88,8 +87,11 @@ export default function VendingCard({machine}: {machine: Machine}) {
|
||||
<p className="text-xl md:text-2xl font-bold text-primary mt-2">{machine.priceLabel}</p>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 mt-2 ">
|
||||
<QuantityControl product={machine}/>
|
||||
<ProtectedLink href={`/products/${machine.id}`} className="px-5 py-3 rounded-xl border border-subtle text-primary hover:text-brand-400">
|
||||
<QuantityControl product={machine} />
|
||||
<ProtectedLink
|
||||
href={`/products/${machine.id}`}
|
||||
className="px-5 py-3 rounded-xl border border-subtle text-primary hover:text-brand-400"
|
||||
>
|
||||
{commerceCopy[locale].details}
|
||||
</ProtectedLink>
|
||||
</div>
|
||||
|
||||
@@ -44,35 +44,81 @@ export default function AuthModal() {
|
||||
await auth.loginPassword(currentPhone, values.password || '');
|
||||
}
|
||||
} catch (caught) {
|
||||
setError(caught instanceof Error && caught.message === 'INVALID_PHONE' ? copy.invalidPhone : copy.invalidCredentials);
|
||||
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'}}>
|
||||
<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
|
||||
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/>}/>
|
||||
<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>
|
||||
<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/>}/>
|
||||
<Input dir="ltr" size="large" maxLength={5} prefix={<SafetyCertificateOutlined />} />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -5,7 +5,17 @@ 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}) {
|
||||
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>) => {
|
||||
@@ -15,5 +25,9 @@ export default function ProtectedLink({href, children, className, onClick}: {hre
|
||||
auth.openLogin(() => router.push(href));
|
||||
}
|
||||
};
|
||||
return <Link href={href} className={className} onClick={handleClick}>{children}</Link>;
|
||||
return (
|
||||
<Link href={href} className={className} onClick={handleClick}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,16 +21,24 @@ export default function ProtectedPage({children}: {children: ReactNode}) {
|
||||
}
|
||||
}, [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>
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -16,26 +16,46 @@ export default function DashboardShell({children}: {children: ReactNode}) {
|
||||
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/>},
|
||||
{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);
|
||||
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>
|
||||
<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>)}
|
||||
{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>
|
||||
<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>
|
||||
|
||||
@@ -13,27 +13,92 @@ const InvoiceDocument = forwardRef<HTMLDivElement, {order: CustomerOrder}>(({ord
|
||||
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}`;
|
||||
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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -12,15 +12,38 @@ export default function OrderCard({order}: {order: CustomerOrder}) {
|
||||
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>
|
||||
<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>
|
||||
<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>
|
||||
<Link
|
||||
href={`/dashboard/orders/${order.id}`}
|
||||
className="inline-block text-brand-400 font-semibold mt-5 hover:underline"
|
||||
>
|
||||
{copy.details}
|
||||
</Link>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,13 @@ 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';
|
||||
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