// components.jsx — shared UI: nav, footer, brand mark, placeholders, icons const { useState, useEffect, useRef, useMemo, createContext, useContext, Fragment } = React; // ─── App context ─────────────────────────────────────────────────────────── const AppCtx = createContext(null); const useApp = () => useContext(AppCtx); const CURRENCIES = { PLN: { symbol: "zł", suffix: true, rate: 1, decimals: 0 }, EUR: { symbol: "€", suffix: false, rate: 0.23, decimals: 0 }, USD: { symbol: "$", suffix: false, rate: 0.25, decimals: 0 }, }; function formatPrice(plnAmount, currencyCode) { const c = CURRENCIES[currencyCode] || CURRENCIES.PLN; const val = Math.round(plnAmount * c.rate); const num = val.toLocaleString("en-US"); return c.suffix ? `${num} ${c.symbol}` : `${c.symbol}${num}`; } function track(event, params = {}) { if (typeof window.gtag === "function") { window.gtag("event", event, params); } } // ─── Brand mark (logo) ───────────────────────────────────────────────────── function BrandMark({ size = 44, onClick }) { const { lang } = useApp(); const name = "Long Street"; const mark = "L"; return ( {mark} {name} Aparthostel · Kraków ); } // ─── Header / nav ────────────────────────────────────────────────────────── function Header({ transparent = false }) { const { t, lang, setLang, page, navigate } = useApp(); const [scrolled, setScrolled] = useState(false); useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 60); onScroll(); window.addEventListener("scroll", onScroll, { passive: true }); return () => window.removeEventListener("scroll", onScroll); }, []); const links = [ ["home", t.nav.home], ["rooms", t.nav.rooms], ["gallery", t.nav.gallery], ["contact", t.nav.contact], ]; return (
navigate("home")} />
/
); } // ─── Footer ──────────────────────────────────────────────────────────────── function Footer() { const { t, lang, navigate } = useApp(); const yr = new Date().getFullYear(); return ( ); } // ─── Placeholder image ───────────────────────────────────────────────────── // Neutral, on-palette tonal panels — used everywhere a hotel photo would go. function PH({ tone = "warm", label, ratio = "4/3", style = {}, children, icon }) { return (
{icon &&
{icon}
} {label &&
{label}
} {children}
); } function Photo({ src, alt = "", ratio = "4/3", style = {}, children, objectPosition = "center" }) { // Serve a WebP sibling (~60% smaller) when the browser supports it; the // original .jpg stays as the fallback. Every photo on the site renders // through this component, so this is the single place the swap is needed. const webp = /\.jpe?g$/i.test(src) ? src.replace(/\.jpe?g$/i, ".webp") : null; return (
{webp && } {alt} {children}
); } // Each room's photos live in images/rooms//01.jpg, 02.jpg, ... // To add a photo: drop the next-numbered file in the folder and bump the count here. window.ROOM_SLUGS = ["standard-1", "standard-2", "comfort-1", "comfort-2"]; window.ROOM_PHOTO_COUNTS = [4, 2, 2, 6]; window.slugFor = (idx) => window.ROOM_SLUGS[idx] || window.ROOM_SLUGS[0]; window.photosFor = (idx) => { const n = window.ROOM_PHOTO_COUNTS[idx] || 0; const slug = window.slugFor(idx); return Array.from({ length: n }, (_, i) => `images/rooms/${slug}/${String(i + 1).padStart(2, "0")}.jpg`); }; window.roomImg = (idx) => window.photosFor(idx)[0] || ""; // ─── Path-based routing (real, crawlable URLs instead of #fragments) ───────── // Single source of truth mapping a page key (+params) <-> URL path. Used by the // router in app.jsx, the nav links below, applySeo, and the prerender build. window.SITE_ORIGIN = "https://longstreet.pl"; window.LANGS = ["pl", "en"]; // pl = root paths, en = /en/* paths window.DEFAULT_LANG = "pl"; window.pathToRoute = function (pathname) { let parts = (pathname || "/").replace(/^\/+|\/+$/g, "").split("/").filter(Boolean); let lang = window.DEFAULT_LANG; if (parts[0] === "en") { lang = "en"; parts = parts.slice(1); } const base = { lang, params: {} }; if (!parts.length) return { ...base, page: "home" }; if (parts[0] === "rooms" && parts[1]) { const idx = window.ROOM_SLUGS.indexOf(parts[1]); return { ...base, page: "room-detail", params: { roomIdx: idx >= 0 ? idx : 0 } }; } const known = { rooms: "rooms", gallery: "gallery", contact: "contact", reservation: "reservation" }; return { ...base, page: known[parts[0]] || "home" }; }; window.routeToPath = function (page, params, lang) { const p = params || {}; const prefix = lang === "en" ? "/en" : ""; let sub; switch (page) { case "rooms": sub = "/rooms"; break; case "room-detail": sub = "/rooms/" + (window.ROOM_SLUGS[p.roomIdx] || window.ROOM_SLUGS[0]); break; case "gallery": sub = "/gallery"; break; case "contact": sub = "/contact"; break; case "reservation": sub = "/reservation"; break; default: sub = ""; } return (prefix + sub) || "/"; // "/en"+"" -> "/en"; ""+"" -> "/" }; // Every real route the site exposes (drives the sitemap + prerender build). window.SITE_ROUTES = ["home", "rooms", "gallery", "contact", "reservation"] .map((pg) => ({ page: pg, params: {} })) .concat(window.ROOM_SLUGS.map((_, i) => ({ page: "room-detail", params: { roomIdx: i } }))); // ─── Icons ───────────────────────────────────────────────────────────────── const Ic = { arrow: (p = {}) => ( ), arrowL: (p = {}) => ( ), arrowDown: (p = {}) => ( ), bed: (p = {}) => ( ), ruler: (p = {}) => ( ), user: (p = {}) => ( ), window: (p = {}) => ( ), wifi: (p = {}) => ( ), coffee: (p = {}) => ( ), bath: (p = {}) => ( ), key: (p = {}) => ( ), leaf: (p = {}) => ( ), fork: (p = {}) => ( ), book: (p = {}) => ( ), pin: (p = {}) => ( ), calendar: (p = {}) => ( ), check: (p = {}) => ( ), star: (p = {}) => ( ), close: (p = {}) => ( ), diamond: (p = {}) => ( ), }; // ─── Decorative serif diamond divider ────────────────────────────────────── function Divider() { return (
); } // expose globally Object.assign(window, { AppCtx, useApp, CURRENCIES, formatPrice, BrandMark, Header, Footer, PH, Ic, Divider, });