feat(ui): Ernten, Chargen-Nachweis und aktive Navigation
- Ernten: Jahrgang als grosse Serifenzahl mit auslaufender Linie, Karten heben sich mit Honigschimmer, Pfeil und Unterstrich - Ernte-Detail: grosser Titel, mitlaufendes Bild, Fuellstandsbalken je Charge (verfuegbare von abgefuellten Glaesern) - Chargen-Seite (QR-Code auf dem Glas): Echtheitssiegel stempelt sich ein, Zeitleiste mit Waben und sich fuellender Linie, Werte in Serifenschrift - Produktverweise heben sich, gezeichneter Pfeil statt Textzeichen - Fix: aktive Seite in der Kopfzeile zeigte den Unterstrich in Textfarbe statt in Honig (unlayered CSS ueberstimmte die Utility-Klasse) Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
1c5282e84f
commit
467984d027
6 files changed
+167
-52
No files matched your search
@@ -57,11 +57,30 @@ export default async function BatchPage(props: PageProps<"/charge/[code]">) {
|
||||
<div className="py-20">
|
||||
<Container className="max-w-3xl">
|
||||
<ScrollReveal>
|
||||
<div className="flex items-center gap-2 text-honey-dark">
|
||||
<ShieldIcon />
|
||||
<span className="tracking-wide-caps text-xs">{t.chargePage.verifiedBatch}</span>
|
||||
{/* Echtheitssiegel: wer den QR-Code vom Glas scannt, landet hier —
|
||||
das Siegel stempelt sich ein, bevor die Details folgen. */}
|
||||
<div className="flex items-center gap-4">
|
||||
<span
|
||||
className="motion-stamp relative flex h-16 w-14 flex-shrink-0 items-center justify-center text-cream animate-[wh-stamp_0.7s_var(--ease-arrive)_both]"
|
||||
style={{ animationDelay: "150ms" }}
|
||||
>
|
||||
<svg viewBox="0 0 24 26" aria-hidden="true" className="absolute inset-0 h-full w-full">
|
||||
<polygon points="12,1 22.4,7 22.4,19 12,25 1.6,19 1.6,7" fill="var(--color-honey-dark)" />
|
||||
<polygon
|
||||
points="12,3.2 20.5,8.1 20.5,17.9 12,22.8 3.5,17.9 3.5,8.1"
|
||||
fill="none"
|
||||
stroke="var(--color-honey-pale)"
|
||||
strokeWidth="0.6"
|
||||
strokeDasharray="1.2 1"
|
||||
/>
|
||||
</svg>
|
||||
<span className="relative">
|
||||
<ShieldIcon />
|
||||
</span>
|
||||
</span>
|
||||
<span className="tracking-wide-caps text-xs text-honey-dark">{t.chargePage.verifiedBatch}</span>
|
||||
</div>
|
||||
<h1 className="mt-3 text-4xl md:text-5xl text-forest">
|
||||
<h1 className="mt-6 text-5xl md:text-6xl leading-[1] tracking-[-0.015em] text-forest">
|
||||
{batch.harvest.type} {batch.harvest.year}
|
||||
</h1>
|
||||
<p className="mt-4 text-sm text-ink/70">{t.chargePage.provenanceNote(batch.code)}</p>
|
||||
@@ -91,20 +110,32 @@ export default async function BatchPage(props: PageProps<"/charge/[code]">) {
|
||||
</div>
|
||||
|
||||
<ScrollReveal className="mt-14">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<ol className="grid grid-cols-3 gap-4">
|
||||
{steps.map((step, i) => (
|
||||
<div key={step.label} className="relative">
|
||||
<li key={step.label} className="relative">
|
||||
<div className="flex items-center">
|
||||
<span className="flex-shrink-0 w-2 h-2 rounded-full bg-honey" />
|
||||
<svg viewBox="0 0 24 26" aria-hidden="true" className="h-4 w-4 flex-shrink-0">
|
||||
<polygon
|
||||
points="12,1 22.4,7 22.4,19 12,25 1.6,19 1.6,7"
|
||||
fill="var(--color-honey)"
|
||||
stroke="var(--color-honey-dark)"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
{i < steps.length - 1 ? (
|
||||
<span className="ml-1 h-px flex-1 bg-ink/15" aria-hidden="true" />
|
||||
<span className="relative ml-1 h-[2px] flex-1 overflow-hidden rounded-full bg-ink/10" aria-hidden="true">
|
||||
<span
|
||||
className="motion-grow absolute inset-0 origin-left bg-honey animate-[wh-grow_0.6s_var(--ease-arrive)_both]"
|
||||
style={{ animationDelay: `${500 + i * 220}ms` }}
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="mt-3 text-xs tracking-wide-caps text-ink/70">{step.label}</p>
|
||||
<p className="mt-1 text-forest">{step.value}</p>
|
||||
</div>
|
||||
<p className="mt-1 font-serif text-xl text-forest tabular-nums">{step.value}</p>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
</ol>
|
||||
</ScrollReveal>
|
||||
|
||||
<ScrollReveal className="mt-10 border-t border-ink/10 pt-8">
|
||||
|
||||
@@ -45,8 +45,8 @@ export default async function HarvestDetailPage(
|
||||
<Container>
|
||||
<BackLink href="/ernten" label={t.erntenDetailPage.backLabel} />
|
||||
</Container>
|
||||
<Container className="grid md:grid-cols-2 gap-14">
|
||||
<ScrollReveal>
|
||||
<Container className="grid md:grid-cols-2 gap-14 lg:gap-20">
|
||||
<ScrollReveal className="self-start md:sticky md:top-28">
|
||||
<ContentImage
|
||||
src={harvest.images[0] || null}
|
||||
alt={harvest.type}
|
||||
@@ -60,26 +60,38 @@ export default async function HarvestDetailPage(
|
||||
<p className="tracking-wide-caps text-xs text-honey-dark mb-3">
|
||||
{harvest.year} · {formatDate(harvest.harvestDate, locale)}
|
||||
</p>
|
||||
<h1 className="text-4xl text-forest">{harvest.type}</h1>
|
||||
<p className="mt-6 text-ink/75 leading-relaxed">{harvest.description}</p>
|
||||
<h1 className="text-5xl md:text-6xl leading-[1] tracking-[-0.015em] text-forest">{harvest.type}</h1>
|
||||
<p className="mt-8 max-w-[60ch] text-lg text-ink/75 leading-relaxed">{harvest.description}</p>
|
||||
|
||||
<h2 className="mt-10 text-lg text-forest">{t.erntenDetailPage.batchesHeading}</h2>
|
||||
<div className="mt-4 space-y-2">
|
||||
{harvest.batches.map((batch) => (
|
||||
<Link
|
||||
key={batch.id}
|
||||
href={`/charge/${batch.code}`}
|
||||
className="flex items-center justify-between gap-4 border border-ink/10 px-4 py-3 text-sm transition-colors hover:border-forest"
|
||||
>
|
||||
<span className="text-forest">{batch.code}</span>
|
||||
<span className="text-ink/65">
|
||||
{t.erntenDetailPage.glassesAvailable(batch.remainingGlasses, batch.glassCount)}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
<h2 className="mt-12 text-2xl text-forest">{t.erntenDetailPage.batchesHeading}</h2>
|
||||
<div className="mt-5 space-y-3">
|
||||
{harvest.batches.map((batch) => {
|
||||
const fill = batch.glassCount > 0 ? Math.min(1, batch.remainingGlasses / batch.glassCount) : 0;
|
||||
return (
|
||||
<Link
|
||||
key={batch.id}
|
||||
href={`/charge/${batch.code}`}
|
||||
className="group block border border-ink/10 px-5 py-4 transition-[border-color,transform,box-shadow] duration-500 ease-[var(--ease-arrive)] hover:-translate-y-0.5 hover:border-forest/50 hover:shadow-[0_18px_34px_-24px_rgba(36,53,43,0.45)]"
|
||||
>
|
||||
<span className="flex items-center justify-between gap-4 text-sm">
|
||||
<span className="font-serif text-xl text-forest tabular-nums">{batch.code}</span>
|
||||
<span className="text-ink/65 tabular-nums">
|
||||
{t.erntenDetailPage.glassesAvailable(batch.remainingGlasses, batch.glassCount)}
|
||||
</span>
|
||||
</span>
|
||||
{/* Fuellstand der Charge — die Zahl rechts bekommt eine Form. */}
|
||||
<span aria-hidden="true" className="mt-3 block h-1.5 overflow-hidden rounded-full bg-ink/10">
|
||||
<span
|
||||
className="motion-grow block h-full origin-left rounded-full bg-gradient-to-r from-honey-dark to-honey animate-[wh-grow_0.9s_var(--ease-arrive)_both]"
|
||||
style={{ width: `${fill * 100}%`, animationDelay: "250ms" }}
|
||||
/>
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<h2 className="mt-10 text-lg text-forest">{t.erntenDetailPage.productsHeading}</h2>
|
||||
<h2 className="mt-12 text-2xl text-forest">{t.erntenDetailPage.productsHeading}</h2>
|
||||
<div className="mt-4 grid gap-3">
|
||||
{harvest.products.map((product) => (
|
||||
<ProductTeaserLink
|
||||
|
||||
+41
-13
@@ -3,6 +3,7 @@ import Link from "next/link";
|
||||
import { Container, SectionHeading } from "@/components/ui";
|
||||
import { ContentImage } from "@/components/content-image";
|
||||
import { ScrollReveal } from "@/components/scroll-reveal";
|
||||
import { HexMark } from "@/components/honeycomb";
|
||||
import { getHarvests } from "@/lib/data";
|
||||
import { formatMonthYear } from "@/lib/format";
|
||||
import { getLocale } from "@/i18n/locale";
|
||||
@@ -34,29 +35,56 @@ export default async function ErntenPage() {
|
||||
<SectionHeading level="h1" eyebrow={t.erntenPage.eyebrow} title={t.erntenPage.title} />
|
||||
</ScrollReveal>
|
||||
|
||||
<div className="mt-16 space-y-16">
|
||||
<div className="mt-16 space-y-24">
|
||||
{years.map((year) => (
|
||||
<div key={year}>
|
||||
<ScrollReveal>
|
||||
<h2 className="text-2xl text-forest mb-6">{year}</h2>
|
||||
{/* Jahrgang wie auf dem Etikett: grosse Zahl, auslaufende Linie. */}
|
||||
<div className="mb-8 flex items-center gap-6">
|
||||
<h2 className="font-serif text-6xl md:text-7xl leading-none text-honey-dark tabular-nums">{year}</h2>
|
||||
<span aria-hidden="true" className="h-px flex-1 bg-gradient-to-r from-honey/50 to-transparent" />
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-14">
|
||||
{byYear[year].map((harvest, i) => (
|
||||
<ScrollReveal key={harvest.id} delay={(i % 3) * 0.08}>
|
||||
<Link
|
||||
href={`/ernten/${harvest.year}/${encodeURIComponent(harvest.type)}`}
|
||||
className="group block"
|
||||
>
|
||||
<ContentImage
|
||||
src={harvest.images[0] || null}
|
||||
alt={harvest.type}
|
||||
tone="forest"
|
||||
className="aspect-[4/3] w-full transition-transform duration-500 group-hover:scale-[1.02]"
|
||||
label={formatMonthYear(harvest.harvestDate, locale)}
|
||||
sizes="(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw"
|
||||
/>
|
||||
<h3 className="mt-4 text-lg text-forest">{harvest.type}</h3>
|
||||
<p className="text-sm text-ink/65">{t.erntenPage.batchCount(harvest.batches.length)}</p>
|
||||
<span className="relative block overflow-hidden rounded-lg transition-[transform,box-shadow] duration-500 ease-[var(--ease-arrive)] group-hover:-translate-y-1 group-hover:shadow-[0_28px_50px_-24px_rgba(36,53,43,0.5)]">
|
||||
<ContentImage
|
||||
src={harvest.images[0] || null}
|
||||
alt={harvest.type}
|
||||
tone="forest"
|
||||
className="aspect-[4/3] w-full transition-transform duration-700 ease-[var(--ease-arrive)] group-hover:scale-[1.05]"
|
||||
label={formatMonthYear(harvest.harvestDate, locale)}
|
||||
sizes="(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw"
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 -translate-x-full bg-[linear-gradient(115deg,transparent_30%,rgba(228,207,164,0.3)_48%,rgba(184,138,68,0.15)_55%,transparent_70%)] transition-transform duration-1000 ease-[var(--ease-arrive)] group-hover:translate-x-full motion-reduce:hidden"
|
||||
/>
|
||||
</span>
|
||||
<div className="mt-5 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h3 className="link-draw inline pb-0.5 font-serif text-2xl text-forest">{harvest.type}</h3>
|
||||
<p className="mt-2 flex items-center gap-2 text-sm text-ink/65">
|
||||
<HexMark className="h-2 w-2 text-honey" />
|
||||
{t.erntenPage.batchCount(harvest.batches.length)}
|
||||
</p>
|
||||
</div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 24 24"
|
||||
className="mt-2 h-4 w-4 flex-shrink-0 text-forest transition-transform duration-300 ease-[var(--ease-arrive)] group-hover:translate-x-1"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
>
|
||||
<path d="M4 12h15M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
</Link>
|
||||
</ScrollReveal>
|
||||
))}
|
||||
|
||||
+28
-2
@@ -121,10 +121,14 @@ p {
|
||||
transition: background-size 350ms var(--ease-arrive);
|
||||
}
|
||||
.link-draw:hover,
|
||||
.link-draw:focus-visible,
|
||||
.link-draw[aria-current="page"] {
|
||||
.link-draw:focus-visible {
|
||||
background-size: 100% 1px;
|
||||
}
|
||||
/* Aktive Seite: kraeftigerer Unterstrich in Honig statt in Textfarbe. */
|
||||
.link-draw[aria-current="page"] {
|
||||
background-image: linear-gradient(var(--color-honey), var(--color-honey));
|
||||
background-size: 100% 2px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.link-draw {
|
||||
@@ -257,3 +261,25 @@ input::placeholder,
|
||||
textarea::placeholder {
|
||||
color: color-mix(in oklab, var(--color-ink) 45%, transparent);
|
||||
}
|
||||
|
||||
/* Chargen-Seite: das Echtheitssiegel "stempelt" sich beim Oeffnen ein. */
|
||||
@keyframes wh-stamp {
|
||||
0% {
|
||||
transform: scale(1.6) rotate(-18deg);
|
||||
opacity: 0;
|
||||
}
|
||||
60% {
|
||||
transform: scale(0.94) rotate(3deg);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1) rotate(0deg);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.motion-stamp {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user