/* global React */
const { useEffect, useRef, useState } = React;
/* ---------- Nav ---------- */
function Nav() {
const { t } = useLang();
return (
Wine Exchange
);
}
/* ---------- Hero (parallax landscape) ---------- */
function Hero() {
const { t } = useLang();
const sceneRef = useRef(null);
useEffect(() => {
const scene = sceneRef.current;
if (!scene) return;
const field = scene.querySelector(".px-layer--vineyard");
if (!field) return;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let raf = 0;
let mx = 0, my = 0, tmx = 0, tmy = 0;
// Only the field (vineyard) animates — it drifts and scales from its centre.
const apply = () => {
mx += (tmx - mx) * 0.08;
my += (tmy - my) * 0.08;
const sc = window.scrollY || window.pageYOffset || 0;
const scrollShift = sc * 0.1;
const grow = Math.min(sc, 600) / 600 * 0.06;
const mxShift = mx * 22;
const myShift = my * 10;
field.style.transform =
`translate3d(${mxShift.toFixed(1)}px, ${(myShift - scrollShift).toFixed(1)}px, 0) scale(${(1.08 + grow).toFixed(3)})`;
const settling = Math.abs(tmx - mx) > 0.001 || Math.abs(tmy - my) > 0.001;
raf = settling ? requestAnimationFrame(apply) : 0;
};
const kick = () => { if (!raf) raf = requestAnimationFrame(apply); };
const onScroll = () => kick();
const onMove = (e) => {
if (reduce) return;
const r = scene.getBoundingClientRect();
tmx = (e.clientX - r.left) / r.width - 0.5;
tmy = (e.clientY - r.top) / r.height - 0.5;
kick();
};
apply();
requestAnimationFrame(() => scene.classList.add("is-in"));
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", onScroll);
if (!reduce) window.addEventListener("mousemove", onMove);
return () => {
window.removeEventListener("scroll", onScroll);
window.removeEventListener("resize", onScroll);
window.removeEventListener("mousemove", onMove);
if (raf) cancelAnimationFrame(raf);
};
}, []);
return (
);
}
/* ---------- Marquee ---------- */
function Marquee() {
const { t } = useLang();
const items = t.marquee;
const icons = [
"images/icon-bottle.png",
"images/icon-grapes.png",
"images/icon-drop.png",
"images/icon-glass.png"];
const row =
<>
{items.map((w, i) =>
{w}
)}
>;
return (
);
}
/* ---------- Section header helper ---------- */
/* Splits heading children on into individually-masked lines */
function MaskLines({ children }) {
const lines = [];
let cur = [];
React.Children.forEach(children, (ch) => {
if (ch && ch.type === "br") {
lines.push(cur);
cur = [];
} else {
cur.push(ch);
}
});
if (cur.length) lines.push(cur);
return lines.map((line, i) =>
{line}
);
}
function SectionHead({ num, children }) {
return (
{num}
{children}
);
}
/* ---------- About ---------- */
/* ---------- About + Week (merged section) ---------- */
const PROCESS = [
{ day: "Monday", title: "Immersion", desc: "We walk the vines at dawn, sit with the story, and work out who the wine is really for. ", loc: "Day one" },
{ day: "Tuesday", title: "Strategy", desc: "We settle on what the wine is and — more usefully — what it isn't. ", loc: "Day two" },
{ day: "Wednesday", title: "Identity", desc: "Logo and identity, drawn by hand, then built into type, grid and palette. A name turns up too, if you need one.", loc: "Day three" },
{ day: "Thursday", title: "Packaging", desc: "Label, capsule, neck, case. We mock the bottle up at full size and carry it through every light in the building.", loc: "Day four" },
{ day: "Friday", title: "Refinement", desc: "Edits made standing up. Proofs pulled, papers tested wet, the whole system written into a printed dossier left on your table.", loc: "Day five" },
{ day: "Sat / Sun", title: "Celebration", desc: "The work rests. We open a bottle of yours, mocked up at scale in its new dress, with the family and the harvest crew. A long table, a short speech.", loc: "Day six & seven", weekend: true }];
function ProcessCarousel() {
const { t } = useLang();
const PROCESS = t.week.cards;
const trackRef = useRef(null);
const scrollBy = (dir) => {
const el = trackRef.current;
if (!el) return;
const step = el.clientWidth * 0.7;
el.scrollBy({ left: dir * step, behavior: "smooth" });
};
return (
{t.week.eyebrowPrefix}{t.week.taglineB}
{t.week.title}
scrollBy(-1)}>
scrollBy(1)}>
{PROCESS.map((row, i) =>
{row.day}
{row.title}.
{row.desc}
{row.loc}
)}
{t.week.end.day}
{t.week.end.title}.
{t.week.end.desc}
{t.week.end.loc}
{t.week.hint}
);
}
function About() {
const { t } = useLang();
return (
{brLines(t.about.heading)}
{t.about.ledeA}{t.about.ledeEm} {t.about.ledeB}
{t.about.p1}
{t.about.p2}
{t.about.mono}
);
}
/* ---------- Philosophy ---------- */
function Philosophy() {
const { t } = useLang();
return (
{t.philosophy.l1a}{t.philosophy.em1.replace(/ /g, "\u00a0")} .
{t.philosophy.l2a}{t.philosophy.em2.replace(/ /g, "\u00a0")} .
{t.philosophy.attrib}
);
}
/* ---------- Team ---------- */
function Team() {
const { t } = useLang();
return (
{brLines(t.team.heading)}
{t.team.ledePre}
andstudio
{t.team.ledePost1}
{t.team.ledePost2}
{t.team.sub}
{t.team.figcaption}
);
}
/* ---------- Comparison (Wine Exchange vs Agency) ---------- */
function Comparison() {
const { t } = useLang();
const COMPARE = t.compare.rows;
return (
{brLines(t.compare.heading)}
{t.compare.oursHead}
{COMPARE.map((r, i) =>
{r.ours}
)}
{t.compare.theirsHead}
{COMPARE.map((r, i) =>
{r.theirs}
)}
);
}
/* ---------- Two Paths (winery / studio open call) ---------- */
function TwoPaths() {
const { t } = useLang();
return (
{t.paths.heading}
{t.paths.lede}
{t.paths.list.map((item, i) =>
{String(i + 1).padStart(2, "0")} {item}
)}
);
}
/* ---------- CTA ---------- */
function CtaBlock() {
const { t } = useLang();
return (
);
}
/* ---------- Footer ---------- */
function Footer() {
const { t } = useLang();
return (
);
}
/* ---------- Tweaks ---------- */
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"palette": "light",
"grain": 0.04
} /*EDITMODE-END*/;
function TweaksUI() {
const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
useEffect(() => {
if (t.palette === "light") document.body.removeAttribute("data-palette");else
document.body.setAttribute("data-palette", t.palette);
}, [t.palette]);
useEffect(() => {
document.documentElement.style.setProperty("--grain", t.grain);
}, [t.grain]);
return (
setTweak("palette", v)}
options={[
{ value: "light", label: "Light" },
{ value: "black", label: "Black" },
{ value: "oxblood", label: "Oxblood" }]
} />
setTweak("grain", v / 100)}
min={0} max={20} step={1} unit="%" />
);
}
/* ---------- Cookie / Privacy Banner ---------- */
function PrivacyBanner() {
const { t } = useLang();
const [dismissed, setDismissed] = useState(() => {
try {
return localStorage.getItem("we-privacy-ack") === "1";
} catch (_) {
return false;
}
});
const [open, setOpen] = useState(false);
if (dismissed) return null;
const accept = () => {
try {localStorage.setItem("we-privacy-ack", "1");} catch (_) {}
setDismissed(true);
};
return (
{t.privacy.copy}
setOpen((v) => !v)}
aria-expanded={open}>
{open ? t.privacy.less : t.privacy.more}
{open &&
{t.privacy.detail}
hello@winedesign.exchange .
}
{t.privacy.accept}
);
}
/* ---------- App ---------- */
function App() {
// Apply defaults on first mount
useEffect(() => {
if (TWEAK_DEFAULTS.palette !== "light") {
document.body.setAttribute("data-palette", TWEAK_DEFAULTS.palette);
}
document.documentElement.style.setProperty("--grain", TWEAK_DEFAULTS.grain);
}, []);
// Scroll-driven reveals + title parallax (rAF; reliable across environments)
useEffect(() => {
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const fades = Array.from(document.querySelectorAll(".reveal"));
const titles = Array.from(document.querySelectorAll("[data-reveal]"));
const parallax = Array.from(document.querySelectorAll("[data-parallax]"));
let raf = 0;
const tick = () => {
const vh = window.innerHeight;
const trigger = vh * 0.88;
fades.forEach((el) => {
if (!el.classList.contains("in") && el.getBoundingClientRect().top < trigger) {
el.classList.add("in");
}
});
titles.forEach((el) => {
if (!el.classList.contains("is-in") && el.getBoundingClientRect().top < trigger) {
el.classList.add("is-in");
}
});
if (!reduce) {
parallax.forEach((n) => {
const f = parseFloat(n.getAttribute("data-parallax")) || 0.04;
const r = n.getBoundingClientRect();
const center = r.top + r.height / 2;
const off = (center - vh / 2) * -f;
n.style.setProperty("--py", `${off.toFixed(1)}px`);
});
}
raf = 0;
};
const onScroll = () => {
if (!raf) raf = requestAnimationFrame(tick);
};
// Defer the first reveal pass two frames so on-load (above-the-fold)
// titles paint their hidden state first, then animate in — otherwise the
// class lands in the same frame as first paint and the transition is skipped.
requestAnimationFrame(() => requestAnimationFrame(tick));
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", onScroll);
// Safety net: if rAF is throttled (e.g. tab not focused on load), make sure
// titles never stay hidden — reveal everything after a short delay. When the
// tab is active this still animates (adding the class triggers the transition).
const safety = setTimeout(() => {
document.querySelectorAll("[data-reveal]").forEach((el) => el.classList.add("is-in"));
document.querySelectorAll(".reveal").forEach((el) => el.classList.add("in"));
}, 1400);
return () => {
window.removeEventListener("scroll", onScroll);
window.removeEventListener("resize", onScroll);
clearTimeout(safety);
if (raf) cancelAnimationFrame(raf);
};
}, []);
return (
<>
>);
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render( );