// Product, content, and auth data for the storefront CMS.
const IMG = (id, w = 800) => `https://images.unsplash.com/${id}?auto=format&fit=crop&w=${w}&q=80`;
const PEXVID = (id, file) => `https://videos.pexels.com/video-files/${id}/${id}-${file}.mp4`;
const PEXPOSTER = (id) => `https://images.pexels.com/videos/${id}/pexels-photo-${id}.jpeg?auto=compress&cs=tinysrgb&w=1400`;
// Reliable editorial fallback (the old local file 404'd on deploy). Editable in admin.
const LOCAL_EDITORIAL = IMG('photo-1469334031218-e382a71b716b', 900);
const SW = {
blush: { name: 'Blush', hex: '#F6D6DA' },
rose: { name: 'Rose', hex: '#B86A78' },
ivory: { name: 'Ivory', hex: '#FFF8F5' },
champagne: { name: 'Champagne', hex: '#F2E3DC' },
deep: { name: 'Cocoa', hex: '#4A2D33' },
sand: { name: 'Sand', hex: '#D8C3A8' },
sage: { name: 'Sage', hex: '#B6C2A7' },
black: { name: 'Onyx', hex: '#1A1414' },
};
const SIZES = ['XS', 'S', 'M', 'L', 'XL'];
const DEFAULT_SITE_DATA = {
settings: {
brandName: 'House of Sheylas',
brandMark: 'HOS',
brandTagline: 'HOUSE OF SHEYLAS',
tagline:
'Quietly elegant pieces, curated for women who dress for themselves.',
supportEmail: 'care@houseofsheylas.com',
pressEmail: 'press@houseofsheylas.com',
phone: '+1 (438) 555-0148',
instagramHandle: '@houseofsheylas',
instagramUrl: 'https://www.instagram.com/',
tiktokUrl: 'https://www.tiktok.com/',
facebookUrl: 'https://www.facebook.com/',
pinterestUrl: 'https://www.pinterest.com/',
copyright:
'© 2026 House of Sheylas. Curated with care in Paris.',
footerLocation: 'Paris',
newsletterCode: 'HOUSE10',
newsletterReplyTo: 'care@houseofsheylas.com',
adminUsername: 'admin',
adminPassword: 'admin123',
},
navigation: [
{ id: 'home', label: 'Home', href: '#home', public: true },
{ id: 'shop', label: 'Shop', href: '#shop', public: true },
{ id: 'collections', label: 'Collections', href: '#collections', public: true },
{ id: 'about', label: 'About', href: '#about', public: true },
{ id: 'contact', label: 'Contact', href: '#contact', public: true },
],
pages: [
{ id: 'home', title: 'Home', status: 'live', href: '#home' },
{ id: 'shop', title: 'Shop', status: 'live', href: '#shop' },
{ id: 'collections', title: 'Collections', status: 'live', href: '#collections' },
{ id: 'about', title: 'About', status: 'live', href: '#about' },
{ id: 'contact', title: 'Contact', status: 'live', href: '#contact' },
],
hero: {
eyebrow: 'Summer · Édition 2026',
title: 'Soft elegance',
accent: 'for every woman.',
description:
'A curated edit of feminine pieces — selected to make you feel confident, graceful and unforgettable.',
metaLeft: 'Boutique · Paris · Est. 2025',
metaRight: 'N°08 · Summer',
primaryCtaLabel: 'Shop new collection',
primaryCtaHref: '#shop/new',
secondaryCtaLabel: 'View lookbook',
secondaryCtaHref: '#collections',
featuredProductId: 'na1',
featuredLabel: 'Featured',
},
story: {
eyebrow: 'Our story',
title: 'For women who love',
accent: 'elegance, confidence,',
end: 'and timeless femininity.',
body:
'House of Sheylas is a curated boutique, bringing together feminine, elevated pieces — chosen with care for the women who wear them.',
imagePrimary: IMG('photo-1496747611176-843222e1e57c', 900),
imageSecondary: LOCAL_EDITORIAL,
stats: [
{ label: 'Launched', value: 'Paris · 2025' },
{ label: 'Atelier', value: 'Small-batch pieces' },
{ label: 'Where we are', value: 'Paris · Canada · Tunisia' },
],
ctaLabel: 'Discover the house',
ctaHref: '#about',
},
newsletter: {
eyebrow: 'Become a member',
title: 'Join the',
accent: 'House',
description:
'Get early access to new drops, private sales, and styling inspiration delivered with care to your inbox.',
successTitle: 'Welcome home.',
},
about: {
eyebrow: 'The House',
title: 'Quietly made,',
accent: 'always intentional.',
subtitle:
'House of Sheylas is an independent womenswear boutique — a curated edit of feminine, elevated pieces selected for women who dress for themselves.',
values: [
{
id: '01',
title: 'Curated selection',
description: 'Every piece is hand-picked. We edit so you don’t have to.',
},
{
id: '02',
title: 'Elevated quality',
description: 'Silk, linen, cashmere — fabrics that breathe, drape, and live with you.',
},
{
id: '03',
title: 'Feminine by design',
description:
'Soft silhouettes and refined details, chosen for how they make you feel.',
},
{
id: '04',
title: 'A quiet ethos',
description: 'No trends, no noise. We select for the woman, not the season.',
},
],
},
locations: [
{
city: 'Paris',
role: 'Studio',
note: 'Where it all began — 2025.',
},
{
city: 'Canada',
role: 'Operations',
note: 'Serving our North American community.',
},
{
city: 'Tunisia',
role: 'Office',
note: 'Part of our team, behind the scenes.',
},
],
services: [
{ id: 'shipping', title: 'Shipping & Returns', href: '#contact' },
{ id: 'tracking', title: 'Order Tracking', href: '#contact' },
{ id: 'size-guide', title: 'Size Guide', href: '#contact' },
{ id: 'care', title: 'Garment Care', href: '#contact' },
{ id: 'contact-us', title: 'Contact Us', href: '#contact' },
],
contact: {
eyebrow: 'Contact',
title: 'We’d love to',
accent: 'hear from you.',
subtitle:
'For styling questions, sizing help, press, or quiet hellos. We answer every message within two working days.',
responseTime: 'We typically reply within 48h.',
topics: [
{ id: 'general', label: 'General enquiry' },
{ id: 'styling', label: 'Styling help' },
{ id: 'order', label: 'Order question' },
{ id: 'press', label: 'Press' },
{ id: 'wholesale', label: 'Wholesale' },
],
faqs: [
{
q: 'Where do you ship?',
a: 'We ship worldwide from Paris. Rates calculated at checkout.',
},
{
q: 'When will I be charged?',
a: 'At checkout. Final taxes and duties calculated by destination.',
},
{
q: 'Do you do exchanges?',
a: 'Yes — within 14 days. Reach out to our care team to arrange one.',
},
{
q: 'Sustainability?',
a: 'We curate elevated pieces and favour natural fibres wherever we can. Read more on our About page.',
},
],
},
collections: [
{
id: 'spring-bloom',
name: 'Summer Edit',
season: 'SS 2026',
pieces: 32,
img: IMG('photo-1490481651871-ab68de25d43d', 1000),
accent: '#F6D6DA',
},
{
id: 'soft-noir',
name: 'Soft Noir',
season: 'AW 2026',
pieces: 18,
img: IMG('photo-1539008835657-9e8e9680c956', 1000),
accent: '#4A2D33',
},
{
id: 'champagne',
name: 'Champagne Hour',
season: 'Resort',
pieces: 24,
img: IMG('photo-1518049362265-d5b2a6b00b37', 1000),
accent: '#F2E3DC',
},
{
id: 'atelier',
name: 'Atelier Edit',
season: 'Capsule',
pieces: 12,
img: LOCAL_EDITORIAL,
accent: '#B86A78',
},
],
categories: [
{
id: 'dresses',
name: 'Dresses',
count: '48 pieces',
img: IMG('photo-1539109136881-3be0616acf4b', 700),
status: 'live',
subcategories: [
{ id: 'new', name: 'New In', href: '#shop/new' },
{ id: 'bestsellers', name: 'Best Sellers', href: '#shop/bestsellers' },
],
},
{
id: 'sets',
name: 'Sets',
count: '24 pieces',
img: IMG('photo-1515886657613-9f3515b0c78f', 700),
status: 'live',
subcategories: [],
},
{
id: 'tops',
name: 'Tops',
count: '36 pieces',
img: IMG('photo-1551803091-e20673f15770', 700),
status: 'live',
subcategories: [],
},
{
id: 'accessories',
name: 'Accessories',
count: '52 pieces',
img: IMG('photo-1606760227091-3dd870d97f1d', 700),
status: 'live',
subcategories: [],
},
],
products: [
{
id: 'na1',
name: 'Aurelie Silk Slip Dress',
price: 248,
category: 'dresses',
img: IMG('photo-1566174053879-31528523f8ae', 700),
tag: 'New',
colors: [SW.blush, SW.champagne, SW.deep],
description:
'Cut from feather-light silk satin, this piece drapes effortlessly with a bias-cut silhouette that moves with you.',
stock: 24,
status: 'live',
isNew: true,
sizes: SIZES,
},
{
id: 'na2',
name: 'Mireille Pleated Skirt',
price: 184,
category: 'bottoms',
img: IMG('photo-1581338834647-b0fb40704e21', 700),
tag: 'New',
colors: [SW.ivory, SW.rose, SW.sand],
stock: 18,
status: 'live',
isNew: true,
sizes: SIZES,
},
{
id: 'na3',
name: 'Celeste Linen Co-ord',
price: 295,
category: 'sets',
img: IMG('photo-1572804013309-59a88b7e92f1', 700),
tag: 'Just in',
colors: [SW.champagne, SW.sage],
stock: 12,
status: 'live',
isNew: true,
sizes: SIZES,
},
{
id: 'na4',
name: 'Sienna Wrap Blouse',
price: 148,
category: 'tops',
img: IMG('photo-1551163943-3f7053a3b8d8', 700),
tag: 'New',
colors: [SW.blush, SW.ivory, SW.rose],
stock: 28,
status: 'live',
isNew: true,
sizes: SIZES,
},
{
id: 'na5',
name: 'Romy Cropped Cardigan',
price: 168,
category: 'tops',
img: IMG('photo-1518049362265-d5b2a6b00b37', 700),
tag: 'Limited',
colors: [SW.rose, SW.champagne, SW.deep],
stock: 9,
status: 'live',
isNew: true,
sizes: SIZES,
},
{
id: 'na6',
name: 'Eloise Mini Dress',
price: 228,
category: 'dresses',
img: IMG('photo-1572804013427-4d7ca7268217', 700),
tag: 'New',
colors: [SW.ivory, SW.blush],
stock: 16,
status: 'live',
isNew: true,
sizes: SIZES,
},
{
id: 'na7',
name: 'Lila Wide-Leg Trousers',
price: 198,
category: 'bottoms',
img: IMG('photo-1594633312681-425c7b97ccd1', 700),
tag: 'New',
colors: [SW.champagne, SW.deep, SW.black],
stock: 21,
status: 'live',
isNew: true,
sizes: SIZES,
},
{
id: 'na8',
name: 'Solene Knot Top',
price: 124,
category: 'tops',
img: IMG('photo-1503342217505-b0a15ec3261c', 700),
tag: 'Just in',
colors: [SW.blush, SW.rose, SW.ivory],
stock: 19,
status: 'live',
isNew: true,
sizes: SIZES,
},
{
id: 'bs1',
name: 'Margaux Tailored Blazer',
price: 325,
category: 'tops',
img: IMG('photo-1591047139829-d91aecb6caea', 800),
badge: 'Bestseller',
reviews: 412,
colors: [SW.champagne, SW.deep, SW.black],
stock: 14,
status: 'live',
isBest: true,
sizes: SIZES,
},
{
id: 'bs2',
name: 'Adele Bias Maxi Dress',
price: 368,
category: 'dresses',
img: IMG('photo-1539008835657-9e8e9680c956', 800),
badge: 'Most loved',
reviews: 587,
colors: [SW.blush, SW.ivory, SW.rose],
stock: 11,
status: 'live',
isBest: true,
sizes: SIZES,
},
{
id: 'bs3',
name: 'Camille Cashmere Knit',
price: 285,
category: 'tops',
img: IMG('photo-1583744946564-b52ac1c389c8', 800),
badge: 'Restocked',
reviews: 298,
colors: [SW.champagne, SW.sand, SW.rose],
stock: 22,
status: 'live',
isBest: true,
sizes: SIZES,
},
{
id: 'bs4',
name: 'Juliette Pearl Cardigan',
price: 248,
category: 'tops',
img: IMG('photo-1551488831-00ddcb6c6bd3', 800),
badge: 'Bestseller',
reviews: 341,
colors: [SW.ivory, SW.blush],
stock: 6,
status: 'live',
isBest: true,
sizes: SIZES,
},
{
id: 'ex1',
name: 'Noor Pearl Earrings',
price: 88,
category: 'accessories',
img: IMG('photo-1535632787350-4e68ef0ac584', 700),
colors: [SW.ivory, SW.champagne],
stock: 31,
status: 'live',
sizes: ['One size'],
},
{
id: 'ex2',
name: 'Ophelia Silk Scarf',
price: 68,
category: 'accessories',
img: IMG('photo-1606760227091-3dd870d97f1d', 700),
colors: [SW.blush, SW.rose],
stock: 33,
status: 'live',
sizes: ['One size'],
},
{
id: 'ex3',
name: 'Quilted Mini Bag',
price: 248,
category: 'accessories',
img: IMG('photo-1584917865442-de89df76afd3', 700),
colors: [SW.champagne, SW.deep],
stock: 8,
status: 'live',
sizes: ['One size'],
},
],
instagram: [
IMG('photo-1485968579580-b6d095142e6e', 600),
IMG('photo-1483985988355-763728e1935b', 600),
LOCAL_EDITORIAL,
IMG('photo-1503342217505-b0a15ec3261c', 600),
],
};
const SITE_DATA_KEY = 'roudaina.siteData.v1';
const ADMIN_SESSION_KEY = 'roudaina.admin.session.v1';
const deepClone = (value) => JSON.parse(JSON.stringify(value));
const slugify = (value) => String(value || '')
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
function normalizeSiteData(raw) {
const site = deepClone(raw || DEFAULT_SITE_DATA);
site.categories = (site.categories || []).map((category) => ({
...category,
status: category.status || 'live',
subcategories: (category.subcategories || []).map((sub) => ({
...sub,
href: sub.href || `#shop/${sub.id}`,
})),
}));
site.products = (site.products || []).map((product) => ({
status: 'live',
stock: 0,
description:
'Cut from feather-light silk satin, this piece drapes effortlessly with a bias-cut silhouette that moves with you.',
sizes: SIZES,
slug: product.slug || slugify(product.name || product.id),
...product,
}));
return site;
}
const defaultSiteData = normalizeSiteData(DEFAULT_SITE_DATA);
function mapApiToSiteData(api) {
const s = api.settings || {};
// Strip leftover placeholder branding ("Roudaina") and any production-house wording
// ("couture", "maison") — House of Sheylas is a curated boutique that sells, not produces.
const clean = (value, fallback) =>
(!value || /roudaina|couture|maison/i.test(value)) ? fallback : value;
const D = defaultSiteData.settings;
return {
settings: {
brandName: clean(s.brand_name, D.brandName),
brandMark: /roudaina/i.test(s.brand_mark || '') ? D.brandMark : (s.brand_mark || D.brandMark),
brandTagline: clean(s.brand_tagline, D.brandTagline),
tagline: clean(s.tagline, D.tagline),
supportEmail: /roudaina/i.test(s.support_email || '') ? D.supportEmail : (s.support_email || D.supportEmail),
pressEmail: /roudaina/i.test(s.press_email || '') ? D.pressEmail : (s.press_email || D.pressEmail),
phone: s.phone || D.phone,
instagramHandle: /roudaina/i.test(s.instagram_handle || '') ? D.instagramHandle : (s.instagram_handle || D.instagramHandle),
instagramUrl: /roudaina/i.test(s.instagram_url || '') ? D.instagramUrl : (s.instagram_url || D.instagramUrl),
tiktokUrl: s.tiktok_url || D.tiktokUrl,
facebookUrl: s.facebook_url || D.facebookUrl,
pinterestUrl: s.pinterest_url || D.pinterestUrl,
newsletterCode: D.newsletterCode,
newsletterReplyTo: D.newsletterReplyTo,
adminUsername: D.adminUsername,
adminPassword: D.adminPassword,
// Multi-currency (product prices are stored in baseCurrency; convert for display)
baseCurrency: s.base_currency || 'CAD',
currencyRates: (s.currency_rates && typeof s.currency_rates === 'object' && Object.keys(s.currency_rates).length) ? s.currency_rates : { USD: 0.73, EUR: 0.68, TND: 2.30 },
enabledCurrencies: (Array.isArray(s.enabled_currencies) && s.enabled_currencies.length) ? s.enabled_currencies : ['CAD', 'EUR', 'USD', 'TND'],
},
homepage: (s.homepage_content && typeof s.homepage_content === 'object') ? s.homepage_content : {},
hero: {
eyebrow: s.hero_eyebrow || defaultSiteData.hero.eyebrow,
title: s.hero_title || defaultSiteData.hero.title,
accent: s.hero_accent || defaultSiteData.hero.accent,
description: s.hero_description || defaultSiteData.hero.description,
},
story: {
title: s.story_title || defaultSiteData.story.title,
accent: s.story_accent || defaultSiteData.story.accent,
end: s.story_end || defaultSiteData.story.end,
body: s.story_body || defaultSiteData.story.body,
imagePrimary: defaultSiteData.story.imagePrimary,
imageSecondary: defaultSiteData.story.imageSecondary,
},
categories: (api.categories || []).map(c => ({ ...c, subcategories: [] })),
products: (api.products || []).map(p => ({ ...p, sizes: p.sizes || SIZES })),
collections: api.collections || defaultSiteData.collections,
newArrivals: api.newArrivals || [],
bestSellers: api.bestSellers || [],
services: api.services || defaultSiteData.services,
faqs: api.faqs || defaultSiteData.faqs,
locations: api.locations || defaultSiteData.locations,
pages: (api.pages || []).map(p => ({
id: p.slug, title: p.title, href: `#${p.slug}`, status: p.status,
})),
instagram: defaultSiteData.instagram,
newsletter: defaultSiteData.newsletter,
about: defaultSiteData.about,
contact: defaultSiteData.contact,
navigation: defaultSiteData.navigation,
};
}
let HERO_VIDEOS = [
{
id: 'walk',
label: 'Walking to camera',
src: PEXVID('12719017', 'uhd_1440_2560_60fps'),
poster: PEXPOSTER('12719017'),
},
{
id: 'turn',
label: 'Turning away',
src: PEXVID('12719021', 'uhd_1440_2560_60fps'),
poster: PEXPOSTER('12719021'),
},
{
id: 'garden',
label: 'Garden walkway',
src: PEXVID('12719019', 'uhd_1440_2560_60fps'),
poster: PEXPOSTER('12719019'),
},
];
let HERO_VIDEO = HERO_VIDEOS[0].src;
let HERO_VIDEO_POSTER = HERO_VIDEOS[0].poster;
let HERO_MAIN = IMG('photo-1490481651871-ab68de25d43d', 1400);
let HERO_ACCENT = IMG('photo-1469334031218-e382a71b716b', 900);
let STORY_IMG_1 = IMG('photo-1496747611176-843222e1e57c', 900);
let STORY_IMG_2 = LOCAL_EDITORIAL;
let SITE_SETTINGS = defaultSiteData.settings;
let PAGE_CONTENT = defaultSiteData.pages;
let HERO_SECTION = defaultSiteData.hero;
let BRAND_STORY_SECTION = defaultSiteData.story;
let NEWSLETTER_SECTION = defaultSiteData.newsletter;
let ABOUT_CONTENT = defaultSiteData.about;
let CONTACT_CONTENT = defaultSiteData.contact;
let LOCATIONS = defaultSiteData.locations;
let SERVICES = defaultSiteData.services;
let CATEGORIES = defaultSiteData.categories;
let COLLECTIONS = defaultSiteData.collections;
let INSTAGRAM = defaultSiteData.instagram;
let ALL_PRODUCTS = defaultSiteData.products;
let NEW_ARRIVALS = ALL_PRODUCTS.filter((product) => product.isNew);
let BEST_SELLERS = ALL_PRODUCTS.filter((product) => product.isBest);
let EXTRA = ALL_PRODUCTS.filter((product) => !product.isNew && !product.isBest);
let COPY = {
description:
'Cut from feather-light silk satin, this piece drapes effortlessly with a bias-cut silhouette that moves with you. Adjustable straps and a softly cowled neckline make it as elegant for evenings out as it is layered beneath knits.',
fabric: '100% mulberry silk · Dry clean only',
fit: "True to size · Model is 5'9\" wearing size S",
shipping: 'Worldwide shipping · Calculated at checkout',
};
function syncSiteGlobals(site) {
SITE_SETTINGS = site.settings;
PAGE_CONTENT = site.pages;
HERO_SECTION = site.hero;
BRAND_STORY_SECTION = site.story;
NEWSLETTER_SECTION = site.newsletter;
ABOUT_CONTENT = site.about;
CONTACT_CONTENT = site.contact;
LOCATIONS = site.locations;
SERVICES = site.services;
CATEGORIES = site.categories;
COLLECTIONS = site.collections;
INSTAGRAM = site.instagram;
ALL_PRODUCTS = site.products;
NEW_ARRIVALS = ALL_PRODUCTS.filter((product) => product.isNew);
BEST_SELLERS = ALL_PRODUCTS.filter((product) => product.isBest);
EXTRA = ALL_PRODUCTS.filter((product) => !product.isNew && !product.isBest);
STORY_IMG_1 = site.story.imagePrimary || STORY_IMG_1;
STORY_IMG_2 = site.story.imageSecondary || STORY_IMG_2;
Object.assign(window, {
IMG,
SW,
SIZES,
HERO_MAIN,
HERO_ACCENT,
HERO_VIDEO,
HERO_VIDEO_POSTER,
HERO_VIDEOS,
STORY_IMG_1,
STORY_IMG_2,
SITE_SETTINGS,
PAGE_CONTENT,
HERO_SECTION,
BRAND_STORY_SECTION,
NEWSLETTER_SECTION,
ABOUT_CONTENT,
CONTACT_CONTENT,
LOCATIONS,
SERVICES,
CATEGORIES,
COLLECTIONS,
INSTAGRAM,
ALL_PRODUCTS,
NEW_ARRIVALS,
BEST_SELLERS,
EXTRA,
COPY,
});
}
function getProduct(id) {
const requested = String(id || '');
return ALL_PRODUCTS.find((product) =>
product.id === requested ||
product.slug === requested ||
slugify(product.name) === requested
) || ALL_PRODUCTS[0];
}
function getCollection(id) {
return COLLECTIONS.find((collection) => collection.id === id) || COLLECTIONS[0];
}
syncSiteGlobals(defaultSiteData);
const SiteDataCtx = createContext(null);
function loadSiteData() {
try {
const raw = localStorage.getItem(SITE_DATA_KEY);
return raw ? normalizeSiteData(JSON.parse(raw)) : defaultSiteData;
} catch {
return defaultSiteData;
}
}
function SiteDataProvider({ children }) {
const [siteData, setSiteData] = useState(loadSiteData);
// Fetch live Supabase data on mount — replaces localStorage/defaults with real content
useEffect(() => {
fetch('/api/site-data')
.then(r => r.json())
.then(api => {
if (api.ok && !api.fallback) {
setSiteData(normalizeSiteData(mapApiToSiteData(api)));
}
})
.catch(() => {});
}, []);
useEffect(() => {
syncSiteGlobals(siteData);
try {
localStorage.setItem(SITE_DATA_KEY, JSON.stringify(siteData));
} catch {}
}, [siteData]);
const updateSiteData = useCallback((updater) => {
setSiteData((current) => normalizeSiteData(typeof updater === 'function' ? updater(current) : updater));
}, []);
const resetSiteData = useCallback(() => {
setSiteData(deepClone(defaultSiteData));
}, []);
const value = useMemo(
() => ({
siteData,
updateSiteData,
resetSiteData,
defaultSiteData,
}),
[siteData, updateSiteData, resetSiteData]
);
return {children};
}
function useSiteData() {
const value = useContext(SiteDataCtx);
if (!value) throw new Error('useSiteData must be used within SiteDataProvider');
return value;
}
const AdminAuthCtx = createContext(null);
function AdminAuthProvider({ children }) {
const { siteData } = useSiteData();
const [authenticated, setAuthenticated] = useState(() => {
try {
return localStorage.getItem(ADMIN_SESSION_KEY) === 'true';
} catch {
return false;
}
});
const login = useCallback(
(username, password) => {
const ok =
username === siteData.settings.adminUsername &&
password === siteData.settings.adminPassword;
if (ok) {
setAuthenticated(true);
try {
localStorage.setItem(ADMIN_SESSION_KEY, 'true');
} catch {}
return { ok: true };
}
return { ok: false, error: 'Invalid username or password.' };
},
[siteData.settings.adminPassword, siteData.settings.adminUsername]
);
const logout = useCallback(() => {
setAuthenticated(false);
try {
localStorage.removeItem(ADMIN_SESSION_KEY);
} catch {}
}, []);
return (
{children}
);
}
function useAdminAuth() {
const value = useContext(AdminAuthCtx);
if (!value) throw new Error('useAdminAuth must be used within AdminAuthProvider');
return value;
}
Object.assign(window, {
IMG,
SW,
SIZES,
COPY,
SiteDataProvider,
useSiteData,
AdminAuthProvider,
useAdminAuth,
ADMIN_SESSION_KEY,
SITE_SETTINGS,
PAGE_CONTENT,
HERO_SECTION,
BRAND_STORY_SECTION,
NEWSLETTER_SECTION,
ABOUT_CONTENT,
CONTACT_CONTENT,
LOCATIONS,
SERVICES,
CATEGORIES,
COLLECTIONS,
INSTAGRAM,
ALL_PRODUCTS,
NEW_ARRIVALS,
BEST_SELLERS,
EXTRA,
getProduct,
getCollection,
HERO_MAIN,
HERO_ACCENT,
HERO_VIDEOS,
HERO_VIDEO,
HERO_VIDEO_POSTER,
STORY_IMG_1,
STORY_IMG_2,
});