/* global React */
const { useState, useEffect, useRef, useMemo, useContext, createContext } = React;
/* =========================================================
App context — cart, lang, density, palette, navigation
========================================================= */
const AppCtx = createContext(null);
const useApp = () => useContext(AppCtx);
/* =========================================================
Hash routing
========================================================= */
function parseHash() {
const raw = (location.hash || "#/").replace(/^#/, "");
const [path, query = ""] = raw.split("?");
const params = Object.fromEntries(new URLSearchParams(query));
const segments = path.split("/").filter(Boolean);
return { path: "/" + segments.join("/"), segments, params };
}
function useRoute() {
const [route, setRoute] = useState(parseHash());
useEffect(() => {
const on = () => { setRoute(parseHash()); window.scrollTo({ top: 0, behavior: "instant" }); };
window.addEventListener("hashchange", on);
return () => window.removeEventListener("hashchange", on);
}, []);
return route;
}
function navigate(path) {
location.hash = "#" + path;
}
/* =========================================================
Currency formatter — USD verbatim (no invention)
========================================================= */
function fmtUSD(n) {
return n.toFixed(2).replace(/\.00$/, "");
}
/* =========================================================
Logo / sparrow mark
========================================================= */
function SparrowMark({ size = 28, color = "currentColor", className = "" }) {
return (
);
}
function Logo({ size = 24, onClick }) {
return (
{ if (onClick) onClick(e); }}>
Orient 499
);
}
/* =========================================================
Navigation
========================================================= */
function Nav() {
const { route, lang, setLang, cart, openCart } = useApp();
const path = route.path;
const cartCount = cart.reduce((s, it) => s + it.qty, 0);
const [mobOpen, setMobOpen] = useState(false);
useEffect(() => { setMobOpen(false); }, [path]);
const links = [
{ p: "/story", en: "The Story", ar: "الحكاية" },
{ p: "/fashion", en: "Fashion", ar: "الأزياء" },
{ p: "/home-decor", en: "Home & Décor", ar: "المنزل والديكور" },
{ p: "/sustainability", en: "Sustainability", ar: "الاستدامة" },
{ p: "/journal", en: "Journal", ar: "اليوميات" },
{ p: "/boutique", en: "Boutique", ar: "البوتيك" },
];
const isActive = (p) => path === p || (p !== "/" && path.startsWith(p));
return (
);
}
/* =========================================================
Footer
========================================================= */
function Footer() {
const { contact, social } = window.O499.contact;
const c = window.O499.contact;
return (
);
}
/* =========================================================
Product card
========================================================= */
function ProductCard({ p, eager = false }) {
return (

{p.ooak &&
One of a kind}
View piece
{p.type} · {p.material.split("·")[0].trim()}
{p.name}
${fmtUSD(p.price)} USD
);
}
/* =========================================================
Cart drawer
========================================================= */
function CartDrawer() {
const { cart, cartOpen, closeCart, addToCart, removeFromCart, setQty } = useApp();
const total = cart.reduce((s, it) => s + it.price * it.qty, 0);
return (
);
}
/* =========================================================
Toast
========================================================= */
function Toast() {
const { toast } = useApp();
return
{toast}
;
}
/* =========================================================
Section header
========================================================= */
function SectionHeader({ chapter, eyebrow, title, right }) {
return (
{chapter && {chapter}}
{eyebrow && {eyebrow}}
{title}
{right &&
{right}
}
);
}
/* =========================================================
Sparrow divider
========================================================= */
function SparrowDivider() {
return (
);
}
/* =========================================================
Marquee strip
========================================================= */
function Marquee({ items }) {
const doubled = items.concat(items);
return (
{doubled.map((t, i) => {t})}
);
}
/* =========================================================
Placeholder (warm-striped) — for flagged missing imagery
========================================================= */
function Placeholder({ label, aspect = "4 / 5", style = {}, stockSeed, stockTags }) {
// If a stockSeed is given, show a real stock photo (Lorem Picsum,
// seeded → deterministic) instead of the striped placeholder, with a small
// "stock stand-in" pill so it stays honest.
if (stockSeed) {
const ratio = (typeof aspect === "string" && aspect.includes("/"))
? aspect.split("/").map(s => parseFloat(s.trim()))
: [4, 5];
const w = 1000, h = Math.round(w * (ratio[1] / ratio[0]));
const src = `https://picsum.photos/seed/${encodeURIComponent(stockSeed)}/${w}/${h}`;
return (
{stockTags || "Stock stand-in · to replace"}
);
}
return (
{label}
);
}
/* =========================================================
Notice (for hours/landline conflicts)
========================================================= */
function Notice({ children }) {
return {children}
;
}
/* Expose to other files */
Object.assign(window, {
AppCtx, useApp, useRoute, navigate, parseHash,
fmtUSD, SparrowMark, Logo, Nav, Footer, ProductCard, CartDrawer,
SectionHeader, SparrowDivider, Marquee, Placeholder, Notice, Toast
});