chore: format project automatically before starting dev server

This commit is contained in:
2026-09-16 14:14:04 +03:30
parent 7c55b3916c
commit 812029fe84
66 changed files with 2088 additions and 1125 deletions
+1 -1
View File
@@ -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
View File
@@ -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>
)}
+20 -5
View File
@@ -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.
}
+12 -3
View File
@@ -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>
);
+27 -32
View File
@@ -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>
);
}
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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. */}
+13 -11
View File
@@ -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>
);
}
+10 -10
View File
@@ -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>
);
}
+40 -40
View File
@@ -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>
);
}
+6 -3
View File
@@ -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>
+41 -14
View File
@@ -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
View File
@@ -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>
);
}
+31 -12
View File
@@ -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>
);
}
+6 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+59 -13
View File
@@ -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>
</>
)}
+16 -2
View File
@@ -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>
);
}
+19 -11
View File
@@ -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;
}
+30 -10
View File
@@ -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>
+78 -13
View File
@@ -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>
+29 -6
View File
@@ -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>
);
}
+8 -1
View File
@@ -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>;
}