200 lines
9.0 KiB
TypeScript
200 lines
9.0 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState } from 'react'
|
|
import { ChevronLeft, ChevronRight, Quote } from 'lucide-react'
|
|
|
|
const voices = [
|
|
{
|
|
quote: `A box of those wok-tossed noodles and the walk home from school stops being just a walk. The chilli catches you at the back of the throat — nothing else on this street does that.`,
|
|
attribution: `Learner, walking home past the corner`,
|
|
detail: `Afternoon rush`,
|
|
},
|
|
{
|
|
quote: `I used to grab a kota every afternoon. Now it is noodles twice a week. Same sort of money, but I can watch it go into the wok and come out steaming.`,
|
|
attribution: `Daily commuter`,
|
|
detail: `Taxi rank run`,
|
|
},
|
|
{
|
|
quote: `Ben still tosses with the same wrist snap his uncle taught him. You can hear the pan over the amapiano from the street.`,
|
|
attribution: `Family, on box duty at the counter`,
|
|
detail: `Weekend shift`,
|
|
},
|
|
{
|
|
quote: `The cabbage still has crunch. That is the part I tell people about — they expect soft noodles, and then they bite into it.`,
|
|
attribution: `Regular since the red sign went up`,
|
|
detail: `Friday takeaway queue`,
|
|
},
|
|
{
|
|
quote: `We drove through from town because someone at work would not stop talking about the dark soy glaze. I did not expect to find it on our own corner.`,
|
|
attribution: `Weekend foodie`,
|
|
detail: `Came through from Newcastle`,
|
|
},
|
|
{
|
|
quote: `My son does not ask for chips anymore. He asks if we are going past the red sign.`,
|
|
attribution: `Parent, school pickup`,
|
|
detail: `Weekday afternoons`,
|
|
},
|
|
]
|
|
|
|
const streetNotes = [
|
|
{
|
|
label: `Rush hour`,
|
|
note: `When the school bell goes, the boxes stack three deep on the counter and nobody minds the wait — it smells too good to complain.`,
|
|
},
|
|
{
|
|
label: `Made in front of you`,
|
|
note: `The wok is loud on purpose. High heat, quick toss, straight into the box while it is still spitting.`,
|
|
},
|
|
{
|
|
label: `Takeaway first`,
|
|
note: `Order on the way past, collect at the corner, eat while it is hot. That is how most of Section 4 does it.`,
|
|
},
|
|
]
|
|
|
|
export default function NeighbourhoodLoveSection() {
|
|
const [index, setIndex] = useState(0)
|
|
const [paused, setPaused] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (paused) return
|
|
const motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)')
|
|
if (motionQuery.matches) return
|
|
const timer = window.setInterval(() => {
|
|
setIndex((current) => (current + 1) % voices.length)
|
|
}, 8000)
|
|
return () => window.clearInterval(timer)
|
|
}, [paused])
|
|
|
|
const goTo = (next: number) => {
|
|
setIndex((next + voices.length) % voices.length)
|
|
}
|
|
|
|
return (
|
|
<section
|
|
id="neighbourhood-love"
|
|
data-vula-section="neighbourhoodlove"
|
|
className="bg-background text-foreground py-24 sm:py-32"
|
|
>
|
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
<div className="max-w-3xl">
|
|
<div className="flex items-center gap-4">
|
|
<span className="h-px w-14 bg-accent" aria-hidden="true" />
|
|
<span className="text-[11px] uppercase tracking-[0.32em] text-muted-foreground">Word of mouth, Section 4</span>
|
|
</div>
|
|
<h2 className="mt-6 text-4xl font-light leading-tight tracking-tight sm:text-5xl">
|
|
The queue talks <span className="italic text-accent">before the wok does.</span>
|
|
</h2>
|
|
<p className="mt-5 max-w-2xl text-base leading-relaxed text-muted-foreground sm:text-lg">
|
|
Nobody wrote these down for us. They came off the pavement outside the red sign — learners on the walk home, commuters waiting on a taxi, neighbours who order the same box every Friday.
|
|
</p>
|
|
</div>
|
|
|
|
<div
|
|
className="mt-14 grid gap-10 lg:grid-cols-12 lg:gap-12"
|
|
onMouseEnter={() => setPaused(true)}
|
|
onMouseLeave={() => setPaused(false)}
|
|
onFocus={() => setPaused(true)}
|
|
onBlur={() => setPaused(false)}
|
|
>
|
|
<div className="lg:col-span-8">
|
|
<div className="relative overflow-hidden rounded-lg border border-foreground/20 bg-foreground/10 p-8 backdrop-blur-md sm:p-12">
|
|
<Quote className="h-7 w-7 text-accent" aria-hidden="true" />
|
|
<div className="relative mt-6 min-h-[340px] sm:min-h-[260px]">
|
|
{voices.map((voice, i) => (
|
|
<figure
|
|
key={voice.attribution}
|
|
aria-hidden={i !== index}
|
|
className={
|
|
i === index
|
|
? 'absolute inset-0 opacity-100 transition-opacity duration-700 ease-out'
|
|
: 'pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-700 ease-out'
|
|
}
|
|
>
|
|
<blockquote className="text-xl font-light leading-relaxed text-foreground sm:text-2xl">
|
|
{voice.quote}
|
|
</blockquote>
|
|
<figcaption className="mt-8 text-[11px] uppercase tracking-[0.28em] text-muted-foreground">
|
|
{voice.attribution}
|
|
<span className="mx-3 text-accent" aria-hidden="true">
|
|
/
|
|
</span>
|
|
{voice.detail}
|
|
</figcaption>
|
|
</figure>
|
|
))}
|
|
</div>
|
|
|
|
<div className="mt-10 flex items-center justify-between gap-6 border-t border-foreground/15 pt-6">
|
|
<div className="flex flex-wrap gap-2">
|
|
{voices.map((voice, i) => (
|
|
<button
|
|
key={voice.attribution}
|
|
type="button"
|
|
onClick={() => setIndex(i)}
|
|
aria-label={`Show neighbourhood voice ${i + 1} of ${voices.length}`}
|
|
aria-current={i === index}
|
|
className={
|
|
i === index
|
|
? 'h-2.5 w-2.5 rounded-full bg-accent transition-all duration-200 ease-out'
|
|
: 'h-2.5 w-2.5 rounded-full bg-foreground/25 transition-all duration-200 ease-out hover:bg-foreground/50'
|
|
}
|
|
/>
|
|
))}
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-[11px] uppercase tracking-[0.28em] text-muted-foreground">
|
|
{String(index + 1).padStart(2, '0')} / {String(voices.length).padStart(2, '0')}
|
|
</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => goTo(index - 1)}
|
|
aria-label="Previous neighbourhood voice"
|
|
className="flex h-11 w-11 items-center justify-center rounded-lg border border-foreground/20 bg-foreground/10 text-foreground backdrop-blur-md transition-all duration-200 ease-out hover:border-accent/60 hover:text-accent"
|
|
>
|
|
<ChevronLeft className="h-5 w-5" aria-hidden="true" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => goTo(index + 1)}
|
|
aria-label="Next neighbourhood voice"
|
|
className="flex h-11 w-11 items-center justify-center rounded-lg border border-foreground/20 bg-foreground/10 text-foreground backdrop-blur-md transition-all duration-200 ease-out hover:border-accent/60 hover:text-accent"
|
|
>
|
|
<ChevronRight className="h-5 w-5" aria-hidden="true" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="lg:col-span-4">
|
|
<div className="rounded-lg border border-foreground/20 bg-foreground/10 p-6 backdrop-blur-md">
|
|
<p className="text-[11px] uppercase tracking-[0.32em] text-accent">Counter rule</p>
|
|
<p className="mt-4 text-sm leading-relaxed text-muted-foreground">
|
|
Say hello when you order. Half the street knows the box by name before the order is finished.
|
|
</p>
|
|
</div>
|
|
<div className="mt-6 rounded-lg border border-foreground/20 bg-foreground/10 p-6 backdrop-blur-md">
|
|
<p className="text-[11px] uppercase tracking-[0.32em] text-accent">Eaten here?</p>
|
|
<p className="mt-4 text-sm leading-relaxed text-muted-foreground">
|
|
Your words are welcome on our wall. Tell us at the counter what the chilli did to you.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<dl className="mt-16 grid border-t border-foreground/15 sm:grid-cols-3">
|
|
{streetNotes.map((note) => (
|
|
<div
|
|
key={note.label}
|
|
className="border-foreground/15 py-8 sm:border-l sm:pl-8 sm:first:border-l-0 sm:first:pl-0"
|
|
>
|
|
<dt className="text-[11px] uppercase tracking-[0.32em] text-accent">{note.label}</dt>
|
|
<dd className="mt-3 text-sm leading-relaxed text-muted-foreground">{note.note}</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|