Initial commit: Craft2Prints with Stages 1-3

This commit is contained in:
Andymick
2026-07-15 18:09:59 +01:00
commit 41937ffc15
158 changed files with 16054 additions and 0 deletions
+131
View File
@@ -0,0 +1,131 @@
'use client';
import Link from 'next/link';
import { useEffect, useState } from 'react';
type NavLink = { href: string; label: string };
type NavSection = { title: string; links: NavLink[] };
export default function MobileNav({
personalisedLinks,
shopBySections,
customer,
}: {
personalisedLinks: NavLink[];
shopBySections: NavSection[];
customer: { name: string | null; email: string } | null;
}) {
const [open, setOpen] = useState(false);
const [expanded, setExpanded] = useState<string | null>(null);
// Lock background scroll while the drawer is open, and always start closed
// on route change (this component stays mounted across client-side nav).
useEffect(() => {
document.body.style.overflow = open ? 'hidden' : '';
return () => {
document.body.style.overflow = '';
};
}, [open]);
const close = () => {
setOpen(false);
setExpanded(null);
};
return (
<div className="md:hidden">
<button
onClick={() => setOpen(true)}
aria-label="Open menu"
aria-expanded={open}
className="flex h-9 w-9 items-center justify-center border border-line text-ink"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M3 6h18M3 12h18M3 18h18" />
</svg>
</button>
{open && (
<div className="fixed inset-0 z-50 flex">
<div className="absolute inset-0 bg-black/30" onClick={close} />
<div className="relative ml-auto flex h-full w-full max-w-xs flex-col overflow-y-auto bg-paper p-6">
<div className="flex items-center justify-between">
<span className="font-script text-3xl leading-none text-ink">Craft2Prints</span>
<button onClick={close} aria-label="Close menu" className="flex h-9 w-9 items-center justify-center border border-line text-ink">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M6 6l12 12M18 6L6 18" />
</svg>
</button>
</div>
<nav className="mt-8 flex flex-col divide-y divide-line">
<div className="py-2">
<button
onClick={() => setExpanded((s) => (s === 'personalised' ? null : 'personalised'))}
aria-expanded={expanded === 'personalised'}
className="flex w-full items-center justify-between py-2 text-left tag-label"
>
Personalised
<span className={`transition-transform ${expanded === 'personalised' ? 'rotate-180' : ''}`}></span>
</button>
{expanded === 'personalised' && (
<div className="flex flex-col pb-2 pl-2">
{personalisedLinks.map((link) => (
<Link key={link.href} href={link.href} onClick={close} className="py-2 text-sm text-muted hover:text-ink">
{link.label}
</Link>
))}
</div>
)}
</div>
<Link href="/products" onClick={close} className="py-4 tag-label">
Products
</Link>
<div className="py-2">
<button
onClick={() => setExpanded((s) => (s === 'shopby' ? null : 'shopby'))}
aria-expanded={expanded === 'shopby'}
className="flex w-full items-center justify-between py-2 text-left tag-label"
>
Shop by
<span className={`transition-transform ${expanded === 'shopby' ? 'rotate-180' : ''}`}></span>
</button>
{expanded === 'shopby' && (
<div className="flex flex-col gap-3 pb-2 pl-2">
{shopBySections
.filter((s) => s.links.length > 0)
.map((section) => (
<div key={section.title}>
<p className="text-xs font-medium uppercase tracking-wide text-muted">{section.title}</p>
<div className="flex flex-col">
{section.links.map((link) => (
<Link key={link.href} href={link.href} onClick={close} className="py-2 text-sm text-muted hover:text-ink">
{link.label}
</Link>
))}
</div>
</div>
))}
{shopBySections.every((s) => s.links.length === 0) && (
<p className="text-sm text-muted">Coming soon.</p>
)}
</div>
)}
</div>
<Link href="/gallery" onClick={close} className="py-4 tag-label">
Gallery
</Link>
<Link href={customer ? '/account' : '/account/login'} onClick={close} className="py-4 tag-label">
{customer ? (customer.name ?? 'My account') : 'Login'}
</Link>
</nav>
</div>
</div>
)}
</div>
);
}