);
}
/* ---------- produktkort (moderne prising, pill-badge) ---------- */
function ProductCard({ badge, tone, icon, title, sub, desc, features, priceLabel, price, cta, onCta, selected, featured }) {
const pill = tone === 'orange' ? 'bg-orange-500' : 'bg-brand';
const hot = featured || selected;
return (
{badge ? (
{badge}
) : null}
{icon ? (
{icon}
) : null}
{title}
{sub ? {sub} : null}
{desc ?
{desc}
: null}
{features && features.length ? (
{features.map((f, i) => (
{f}
))}
) : null}
{priceLabel ? {priceLabel} : null}
{price}
{cta ? (
{cta} →
) : null}
);
}
/* ---------- header / banner ---------- */
// Loud, unmissable bar shown on every non-prod environment so nobody mistakes
// staging for production. It also prints the resolved API endpoints this build
// talks to (handy when juggling dev/prod). Renders NOTHING in prod
// (config.staging === false), so production never exposes its endpoints.
function StagingBanner() {
const { t } = useI18n();
const cfg = (window.STWORDER && window.STWORDER.config) || {};
if (!cfg.staging) return null;
// '' bases mean same-origin — show where that actually resolves to.
const api = cfg.apiBase || (location.origin + '/api');
const domains = cfg.domainApiBase || location.origin;
const env = (cfg.env || 'dev').toUpperCase();
return (
);
}
// Kirby-tvilling: hovedmenyen speiler stw-dev.dynavee.net. Menylenkene peker på
// Kirby-innholdssidene (ekte full-side-navigasjon UT av SPA-en) – butikkens
// transaksjonsruter (#/domener osv.) nås via CTA-er/søk på de sidene, ikke via
// global-navet. SITE byttes dev→prod og blir til slutt samme-origin (/no/...).
// `match` knytter en Kirby-side til SPA-ruten den tilsvarer, så riktig element
// markeres aktivt mens man er inne i flyten. Handlevognen er SPA-ens egen og
// finnes ikke i Kirby-headeren ("behold Handlevogn"). Neste steg: hente menyen
// som JSON fra Kirby (én kilde) i stedet for denne hardkodede speilingen.
const SITE = 'https://stw-dev.dynavee.net';
const LOGIN_URL = 'https://my.servetheworld.net/login?language=norwegian';
function Header({ route, onCart }) {
const { t, lang, setLang } = useI18n();
const cart = useCart();
const links = [
{ href: SITE + '/no/domene/', label: 'Domene', match: 'domener' },
{ href: SITE + '/no/webhotell/', label: 'Webhotell', match: 'hosting' },
{ href: SITE + '/no/wordpress-hosting/', label: 'WordPress' },
{ href: SITE + '/no/epost/', label: 'Epost' },
{ href: SITE + '/no/ssd-vps/', label: 'VPS', match: 'vps' },
{ href: SITE + '/no/dedikert-server/', label: 'Dedikert Server' },
{ href: SITE + '/no/colocation', label: 'Colocation' },
];
const sideLinks = [
{ href: SITE + '/no/blogg/', label: 'Blogg' },
{ href: SITE + '/no/kundesenter/', label: 'Support' },
];
return (
);
}
/* Kasse-nudge: dukker opp etter at hosting legges i vognen ('checkout:nudge'
på bussen) – bekrefter og peker mot kassen uten å tvinge. */
function CheckoutToast() {
const { t } = useI18n();
const [nudge, setNudge] = React.useState(null);
// Nudgen peker mot kassen – meningsløs når du allerede ER i kassen (der
// oppsummeringen oppdateres live og "Til kassen" er rett foran deg). Dropp
// den da, så f.eks. "Legg til webhotell" → avbryt/"Nei takk" ikke spretter
// opp en kasse-pille midt på kasse-siden.
useEffect(() => NS.bus.on('checkout:nudge', (payload) => {
if (location.hash.replace(/^#\/?/, '').startsWith('kasse')) return;
setNudge(payload || {});
}), []);
// Skjul toasten KUN når upsell-dialogen faktisk åpner ('upsell:shown') – aldri
// to lag med nudges. NB: ikke på 'domain:added', for når upsellen er dempet
// ("ikke spør igjen") gir nettopp det eventet en checkout:nudge vi vil beholde.
useEffect(() => NS.bus.on('upsell:shown', () => setNudge(null)), []);
useEffect(() => {
if (!nudge) return;
const id = setTimeout(() => setNudge(null), 7000);
return () => clearTimeout(id);
}, [nudge]);
if (!nudge) return null;
return (