init
This commit is contained in:
commit
c19b910c36
80 changed files with 16134 additions and 0 deletions
123
app/components/RotatingPreview.tsx
Normal file
123
app/components/RotatingPreview.tsx
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
"use client";
|
||||
|
||||
import { Pause, Play, Repeat2, ShoppingBag } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export interface PreviewSet {
|
||||
theme: string;
|
||||
blurb: string;
|
||||
meals: { slot: string; title: string; cost: string }[];
|
||||
total: string;
|
||||
sharedItems: number;
|
||||
itemCount: number;
|
||||
}
|
||||
|
||||
const INTERVAL_MS = 5000;
|
||||
|
||||
export function RotatingPreview({ sets }: { sets: PreviewSet[] }) {
|
||||
const [index, setIndex] = useState(0);
|
||||
const [paused, setPaused] = useState(false);
|
||||
const [hovering, setHovering] = useState(false);
|
||||
const running = !paused && !hovering;
|
||||
|
||||
useEffect(() => {
|
||||
if (!running || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||
const timer = window.setInterval(() => setIndex((i) => (i + 1) % sets.length), INTERVAL_MS);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [running, index, sets.length]);
|
||||
|
||||
const set = sets[index];
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label="Example meal plans"
|
||||
className="relative"
|
||||
onMouseEnter={() => setHovering(true)}
|
||||
onMouseLeave={() => setHovering(false)}
|
||||
onFocus={() => setHovering(true)}
|
||||
onBlur={() => setHovering(false)}
|
||||
>
|
||||
{/* Architectural editorial frame — crisp 1px border & tactile offset shadow instead of blurred glow blobs */}
|
||||
<div className="rounded-xl border border-line-strong bg-surface shadow-[6px_6px_0_0_var(--color-line)]">
|
||||
{/* Top progress rule */}
|
||||
<div className="h-1 overflow-hidden rounded-t-xl bg-raised">
|
||||
<div
|
||||
key={index}
|
||||
className="h-full origin-left animate-preview-timer bg-brand-600"
|
||||
style={{ animationDuration: `${INTERVAL_MS}ms`, animationPlayState: running ? "running" : "paused" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-5 sm:p-6">
|
||||
<div key={set.theme} className="animate-fade-in">
|
||||
<div className="flex items-baseline justify-between gap-3 border-b border-line pb-3">
|
||||
<div>
|
||||
<p className="font-mono text-[11px] tracking-wider text-ink-faint uppercase">Sample menu · 2 people</p>
|
||||
<h2 className="mt-0.5 font-serif text-xl font-semibold tracking-tight text-ink">{set.theme}</h2>
|
||||
</div>
|
||||
<span className="rounded-md border border-line bg-raised px-2.5 py-1 text-xs font-medium text-ink-muted">
|
||||
{set.blurb}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ul className="divide-y divide-line">
|
||||
{set.meals.map((m) => (
|
||||
<li key={m.slot} className="flex items-baseline justify-between gap-4 py-3">
|
||||
<div className="min-w-0">
|
||||
<p className="font-mono text-[11px] tracking-wide text-ink-faint uppercase">{m.slot}</p>
|
||||
<p className="truncate font-medium text-ink">{m.title}</p>
|
||||
</div>
|
||||
<p className="shrink-0 font-mono text-xs text-ink-muted tabular-nums">{m.cost}/meal</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* Receipt tear-off footer */}
|
||||
<div className="receipt-rule mt-2 pt-3.5">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 rounded-lg bg-raised px-3.5 py-2.5 text-xs">
|
||||
<span className="inline-flex items-center gap-1.5 font-medium text-status-ok">
|
||||
<Repeat2 className="size-3.5 shrink-0" strokeWidth={2} aria-hidden />
|
||||
{set.sharedItems} shared store items
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1.5 font-mono font-semibold text-ink tabular-nums">
|
||||
<ShoppingBag className="size-3.5 shrink-0 text-ink-muted" strokeWidth={1.75} aria-hidden />
|
||||
{set.itemCount} pkgs · {set.total}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Accessible 40x40px pagination & playback controls */}
|
||||
<div className="mt-3 flex items-center justify-between border-t border-line pt-2">
|
||||
<div className="flex items-center">
|
||||
{sets.map((s, i) => (
|
||||
<button
|
||||
key={s.theme}
|
||||
type="button"
|
||||
onClick={() => setIndex(i)}
|
||||
aria-label={`Show example: ${s.theme}`}
|
||||
aria-current={i === index}
|
||||
className="grid size-9 place-items-center rounded-md hover:bg-raised"
|
||||
>
|
||||
<span
|
||||
className={`block h-2 rounded-full transition-all ${
|
||||
i === index ? "w-5 bg-brand-600" : "w-2 bg-line-strong"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPaused((p) => !p)}
|
||||
className="trim-cap inline-flex min-h-9 items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium text-ink-muted transition-colors hover:bg-raised hover:text-ink"
|
||||
>
|
||||
{paused ? <Play className="size-3.5" aria-hidden /> : <Pause className="size-3.5" aria-hidden />}
|
||||
{paused ? "Play" : "Pause"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue