- Run record
- loading
Loading this Run…
Loading the Revisions of this Run…
A published source file of Revision 0 of Fjora — Taste the North 1.3.0, shown as text in the browser.
Source is browser-viewable only. There is no download or archive endpoint, and no additional license is granted beyond inspection and research.
import { useState } from "react";
import { createRoot } from "react-dom/client";
import "./styles.css";
import brand from "../public/data/brand.json";
import productData from "../public/data/products.json";
const notes: Record<string, string> = {
"glacier-lime":
"Bright lime peel over a cool line of juniper. Arctic thyme comes last, dry and green. Clean, cold, and quietly sharp.",
cloudberry:
"Soft cloudberry, amber and lightly tart. Chamomile rounds the edges. Rosehip leaves a gentle, floral finish.",
"spruce-tip":
"Fresh spruce tip, resinous and light. Angelica adds a herbal depth. Black currant leaf keeps it green and still.",
};
const faqs = [
{
q: "Is Fjora sweetened?",
a: "No. Every flavor is unsweetened. There is no sugar and there are no sweeteners. The taste comes from the botanicals.",
},
{
q: "Does it contain caffeine?",
a: "No. All three flavors are caffeine-free, so you can pour one at any hour.",
},
{
q: "What are the bottles made of?",
a: "Each bottle is a single material: glass, closed with an aluminium cap. Both are widely recyclable.",
},
{
q: "How long is Taste the North available?",
a: "It is sold seasonally while stock lasts. The line is bottled once per season, then it is gone until the next.",
},
{
q: "What is in the Taste the North line?",
a: "Three winter flavors: Glacier Lime, Cloudberry and Spruce Tip. Each is built on glacial meltwater and cold-steeped botanicals.",
},
];
function Mark({ size = 28 }: { size?: number }) {
return (
<svg width={size} height={size} viewBox="0 0 32 32" aria-hidden="true">
<circle cx="16" cy="16" r="15" fill="none" stroke="currentColor" strokeWidth="1.5" />
<path d="M5 23 L13 10 L17.5 17 L20 13.5 L27 23 Z" fill="currentColor" />
<path d="M9 8 Q16 4 23 8" fill="none" stroke="var(--aurora)" strokeWidth="1.5" strokeLinecap="round" />
</svg>
);
}
function Wordmark({ size = 28 }: { size?: number }) {
return (
<span className="wordmark" aria-label="Fjora">
<Mark size={size} />
<span className="wordmark-text" style={{ fontSize: size * 0.95 }} aria-hidden="true">
Fjora
</span>
</span>
);
}
function scrollToId(id: string) {
const el = document.getElementById(id);
if (!el) return;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
el.scrollIntoView({ behavior: reduce ? "auto" : "smooth", block: "start" });
history.replaceState(null, "", `#${id}`);
}
function Nav({ id, children }: { id: string; children: string }) {
return (
<a
href={`#${id}`}
onClick={(e) => {
e.preventDefault();
scrollToId(id);
}}
>
{children}
</a>
);
}
function Flavors() {
const products = productData.products;
const [selected, setSelected] = useState(products[0].id);
const current = products.find((p) => p.id === selected)!;
return (
<section id="flavors" className="section flavors" aria-labelledby="flavors-h">
<div className="wrap">
<p className="eyebrow">Three flavors. One season.</p>
<h2 id="flavors-h">Choose your flavor</h2>
<div className="picker" role="group" aria-label="Seasonal flavors">
{products.map((p) => (
<button
key={p.id}
type="button"
className="option"
aria-pressed={p.id === selected}
onClick={() => setSelected(p.id)}
>
<img src={p.image} alt="" />
<span>{p.name}</span>
</button>
))}
</div>
<article className="detail" aria-live="polite" data-flavor={current.id}>
<img className="detail-img" src={current.image} alt={`${current.name} bottle`} />
<div className="detail-body">
<p className="eyebrow">Selected flavor</p>
<h3>{current.name}</h3>
<h4>Botanicals</h4>
<ul className="chips">
{current.botanicals.map((b) => (
<li key={b}>{b}</li>
))}
</ul>
<h4>Tasting note</h4>
<p className="note">{notes[current.id]}</p>
</div>
</article>
</div>
</section>
);
}
function Faq() {
const [open, setOpen] = useState<Set<number>>(new Set());
const toggle = (i: number) =>
setOpen((s) => {
const n = new Set(s);
if (n.has(i)) n.delete(i);
else n.add(i);
return n;
});
return (
<section id="faq" className="section faq" aria-labelledby="faq-h">
<div className="wrap narrow">
<p className="eyebrow">Good to know</p>
<h2 id="faq-h">Questions, answered plainly</h2>
<div className="faq-list">
{faqs.map((f, i) => {
const isOpen = open.has(i);
return (
<div className="faq-item" key={f.q}>
<h3>
<button
type="button"
aria-expanded={isOpen}
aria-controls={`faq-a-${i}`}
id={`faq-q-${i}`}
onClick={() => toggle(i)}
>
<span>{f.q}</span>
<svg width="18" height="18" viewBox="0 0 18 18" aria-hidden="true" className="chev">
<path d="M4 7l5 5 5-5" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
</h3>
<div id={`faq-a-${i}`} role="region" aria-labelledby={`faq-q-${i}`} hidden={!isOpen}>
<p>{f.a}</p>
</div>
</div>
);
})}
</div>
</div>
</section>
);
}
function App() {
return (
<>
<header className="topbar">
<a href="#top" className="brandlink" onClick={(e) => { e.preventDefault(); scrollToId("top"); }} aria-label="Fjora, back to top">
<Wordmark size={24} />
</a>
<nav aria-label="Sections">
<Nav id="flavors">Flavors</Nav>
<Nav id="story">Story</Nav>
<Nav id="faq">FAQ</Nav>
</nav>
</header>
<main>
<section id="top" className="hero" aria-labelledby="hero-h">
<div className="hero-bg" aria-hidden="true" />
<div className="wrap hero-inner">
<Wordmark size={44} />
<h1 id="hero-h">Taste the North</h1>
<p className="lede">
Botanical sparkling water from glacial meltwater. Cold-steeped. Bottled once per season.
</p>
<a
className="cta"
href="#flavors"
onClick={(e) => {
e.preventDefault();
scrollToId("flavors");
}}
>
Explore the flavors
<svg width="18" height="18" viewBox="0 0 18 18" aria-hidden="true"><path d="M9 3v11M4 10l5 5 5-5" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" /></svg>
</a>
</div>
</section>
<Flavors />
<section id="story" className="section story" aria-labelledby="story-h">
<div className="wrap story-grid">
<img src="/assets/story-botanicals.webp" alt="Macro photograph of northern botanicals" />
<div>
<p className="eyebrow">Our story</p>
<h2 id="story-h">Water first. Then the botanicals.</h2>
<p>Fjora begins with glacial meltwater. It is cold, clear and still.</p>
<p>We steep botanicals in that cold water. Slowly. Nothing is rushed, and nothing is added that does not belong.</p>
<p>Taste the North is our winter line. Three flavors, bottled once per season. When the season ends, so does the batch.</p>
<p>The result is sparkling water with a Nordic origin and a quiet point of view.</p>
</div>
</div>
</section>
<Faq />
</main>
<footer className="footer">
<div className="wrap footer-inner">
<Wordmark size={24} />
<p>{brand.footer_contact}</p>
<p className="fiction">{brand.fictional_notice}</p>
</div>
</footer>
</>
);
}
createRoot(document.getElementById("root")!).render(<App />);