- 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.2.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 brandData from "../public/data/brand.json";
import productData from "../public/data/products.json";
import "./styles.css";
type Product = (typeof productData.products)[number];
const tastingNotes: Record<Product["id"], string> = {
"glacier-lime":
"Bright lime peel opens first. Juniper follows, dry and cool, with Arctic thyme at the edge.",
cloudberry:
"Soft cloudberry meets a quiet floral note. Rosehip carries the finish with a gentle tartness.",
"spruce-tip":
"Green spruce tip brings a clean forest lift. Angelica and black currant leaf make the finish deep and dry.",
};
const faqItems = [
{
question: "Is the seasonal line sweetened?",
answer: "No. Every Taste the North flavour is unsweetened, with no sugar and no sweeteners.",
},
{
question: "Is there caffeine in Fjora?",
answer: "No. All three seasonal flavours are caffeine-free.",
},
{
question: "What are the bottles made from?",
answer:
"Fjora bottles use single-material glass with an aluminium cap. Both materials are widely recyclable.",
},
{
question: "When is Taste the North available?",
answer: "The line is bottled once per season and sold seasonally while stock lasts.",
},
];
function FjoraMark({ dark = false }: { dark?: boolean }) {
return (
<span className={`wordmark${dark ? " wordmark--dark" : ""}`} aria-label="Fjora">
<svg className="wordmark__rune" viewBox="0 0 32 32" aria-hidden="true">
<path d="M16 2.5v27M4.5 9.4 16 2.5l11.5 6.9M7 22.5 16 29.5l9-7" />
<circle cx="16" cy="16" r="3.15" />
</svg>
<span>fjora</span>
</span>
);
}
function Arrow({ direction = "down" }: { direction?: "down" | "right" }) {
return (
<svg
className={`arrow arrow--${direction}`}
viewBox="0 0 18 18"
fill="none"
aria-hidden="true"
>
<path d="M9 2.5v12M4.5 10.5 9 15l4.5-4.5" />
</svg>
);
}
function App() {
const [selectedId, setSelectedId] = useState<Product["id"]>(productData.products[0].id);
const [menuOpen, setMenuOpen] = useState(false);
const selectedProduct = productData.products.find((product) => product.id === selectedId)!;
const closeMenu = () => setMenuOpen(false);
return (
<main>
<section className="hero" id="top" aria-labelledby="campaign-title">
<div className="hero__image" aria-hidden="true" />
<div className="hero__veil" aria-hidden="true" />
<header className="site-header">
<a className="brand-link" href="#top" aria-label="Fjora, back to top">
<FjoraMark />
</a>
<button
className="menu-button"
type="button"
aria-expanded={menuOpen}
aria-controls="site-navigation"
onClick={() => setMenuOpen((open) => !open)}
>
<span>{menuOpen ? "Close" : "Menu"}</span>
<span className={`menu-button__lines${menuOpen ? " is-open" : ""}`} aria-hidden="true">
<i />
<i />
</span>
</button>
<nav className={`site-nav${menuOpen ? " site-nav--open" : ""}`} id="site-navigation" aria-label="Main navigation">
<a href="#flavors" onClick={closeMenu}>Flavours</a>
<a href="#story" onClick={closeMenu}>Our water</a>
<a href="#faq" onClick={closeMenu}>Questions</a>
</nav>
</header>
<div className="hero__content shell">
<p className="eyebrow eyebrow--light"><span /> Seasonal botanical sparkling water</p>
<h1 id="campaign-title" aria-label="Taste the North">Taste<br />the North</h1>
<div className="hero__bottom">
<p className="hero__intro">
A cold, clear line of botanical sparkling water. Made for the long light and the quiet dark.
</p>
<a className="button button--light" href="#flavors">
Find your flavour <Arrow direction="right" />
</a>
</div>
</div>
<div className="hero__coordinates" aria-hidden="true">
<span>69° 39′ N</span>
<span>18° 57′ E</span>
</div>
<a className="scroll-cue" href="#flavors" aria-label="Scroll to flavours">
<span>Scroll to explore</span>
<Arrow />
</a>
</section>
<section className="flavours section" id="flavors" aria-labelledby="flavours-title">
<div className="shell">
<div className="section-heading flavours__heading">
<div>
<p className="eyebrow"><span /> The winter line</p>
<h2 id="flavours-title">Three clear<br /><em>expressions.</em></h2>
</div>
<p className="section-heading__note">Bottled once each season. Choose a botanical place to begin.</p>
</div>
<div className="flavour-layout">
<div className="flavour-picker" role="group" aria-label="Choose a seasonal flavour">
{productData.products.map((product, index) => {
const selected = product.id === selectedId;
return (
<button
className={`flavour-option${selected ? " flavour-option--selected" : ""}`}
type="button"
key={product.id}
aria-pressed={selected}
onClick={() => setSelectedId(product.id)}
>
<span className="flavour-option__number">0{index + 1}</span>
<span className="flavour-option__image-wrap">
<img src={product.image} alt="" className="flavour-option__image" />
</span>
<span className="flavour-option__copy">
<span>{product.name}</span>
<small>{selected ? "Selected" : "Select"}</small>
</span>
<span className="flavour-option__line" aria-hidden="true" />
</button>
);
})}
</div>
<article className="flavour-detail" aria-live="polite" aria-label={`${selectedProduct.name} tasting details`}>
<div className="flavour-detail__image-wrap">
<span className="image-index" aria-hidden="true">0{productData.products.findIndex((product) => product.id === selectedId) + 1} / 03</span>
<img
className="flavour-detail__image"
src={selectedProduct.image}
alt={`${selectedProduct.name} Fjora botanical sparkling water`}
/>
</div>
<div className="flavour-detail__content">
<p className="eyebrow"><span /> Selected flavour</p>
<h3>{selectedProduct.name}</h3>
<p className="tasting-note">{tastingNotes[selectedProduct.id]}</p>
<div className="botanicals">
<span className="botanicals__label">Botanicals</span>
<ul>
{selectedProduct.botanicals.map((botanical) => <li key={botanical}>{botanical}</li>)}
</ul>
</div>
</div>
</article>
</div>
</div>
</section>
<section className="story section" id="story" aria-labelledby="story-title">
<div className="shell story__grid">
<div className="story__image-wrap">
<img src="/assets/story-botanicals.webp" alt="A close view of Nordic botanicals" className="story__image" />
<span className="story__caption">A slower steep</span>
</div>
<div className="story__copy">
<p className="eyebrow eyebrow--light"><span /> A northern origin</p>
<h2 id="story-title">Water shaped<br />by the <em>cold.</em></h2>
<p>
Fjora begins with a northern story: glacial meltwater and botanicals steeped in the cold.
Nothing hurried. Nothing loud.
</p>
<p>
Taste the North gathers three flavours for winter, each built around a distinct botanical profile.
They are bottled once per season, then allowed to find their way.
</p>
<div className="story__rule" aria-hidden="true"><span /> <i /></div>
<p className="story__small">Cold water. Considered botanicals. A little more room to taste.</p>
</div>
</div>
</section>
<section className="faq section" id="faq" aria-labelledby="faq-title">
<div className="shell faq__layout">
<div className="faq__intro">
<p className="eyebrow"><span /> A little clarity</p>
<h2 id="faq-title">Good to<br /><em>know.</em></h2>
<p>Small details, kept simple.</p>
</div>
<div className="faq__list">
{faqItems.map((item, index) => (
<details className="faq-item" key={item.question}>
<summary>
<span className="faq-item__number">0{index + 1}</span>
<span>{item.question}</span>
<span className="faq-item__icon" aria-hidden="true" />
</summary>
<div className="faq-item__answer"><p>{item.answer}</p></div>
</details>
))}
</div>
</div>
</section>
<footer className="footer">
<div className="shell footer__main">
<FjoraMark />
<div className="footer__contact">
<span>For quiet notes and questions</span>
<span className="contact-line">{brandData.footer_contact}</span>
</div>
<a className="footer__top" href="#top">Back to top <Arrow /></a>
</div>
<div className="shell footer__bottom">
<p>{brandData.fictional_notice}</p>
<p>© Fjora</p>
</div>
</footer>
</main>
);
}
createRoot(document.getElementById("root")!).render(<App />);