// HOS — Admin / back-office panel
// Same brand palette, dense layout, real interactions on mock data.
/* ───────────── Mock back-office data ───────────── */
// Generate 30-day sales series with some natural waviness
const ADMIN_SALES_30D = (() => {
const arr = [];
const today = new Date();
let base = 3200;
for (let i = 29; i >= 0; i--) {
const d = new Date(today); d.setDate(today.getDate() - i);
// gentle sine + noise + weekend lift
const wave = Math.sin(i * 0.42) * 900;
const noise = (Math.random() - 0.4) * 1100;
const weekend = (d.getDay() === 0 || d.getDay() === 6) ? 1200 : 0;
const v = Math.max(900, Math.round(base + wave + noise + weekend));
arr.push({ date: d, value: v });
}
return arr;
})();
const ADMIN_KPIS = (() => {
const total = ADMIN_SALES_30D.reduce((s, d) => s + d.value, 0);
const prevTotal = total * 0.83;
const orders = 312;
return [
{ id:'revenue', label:'Revenue (30d)', value:`$${total.toLocaleString()}`, delta:+18.2, accent:'rose' },
{ id:'orders', label:'Orders (30d)', value: orders.toString(), delta:+12.4, accent:'rose' },
{ id:'aov', label:'Avg. order', value:`$${Math.round(total/orders)}`, delta:+5.1, accent:'rose' },
{ id:'cvr', label:'Conversion', value:'3.42%', delta:-0.6, accent:'rose' },
];
})();
const ADMIN_ORDERS = [
{ id:'1208', customer:'Léa Bernard', email:'lea.b@email.com', total:248, status:'paid', items:1, date:'2026-06-04 · 14:32', country:'FR', address:'12 Rue de Rivoli, 75001 Paris, France', trackingNumber:'', notes:'', lineItems:[{name:'Aurelie Silk Slip Dress', color:'Blush', size:'S', qty:1, price:248, img:'https://images.unsplash.com/photo-1539109136881-3be0616acf4b?w=80&q=60'}] },
{ id:'1207', customer:'Imane Saadi', email:'imane.s@email.com', total:412, status:'paid', items:2, date:'2026-06-04 · 11:08', country:'TN', address:'45 Avenue Habib Bourguiba, 1000 Tunis, Tunisia', trackingNumber:'', notes:'', lineItems:[{name:'Adèle Bias Maxi Dress', color:'Champagne', size:'M', qty:1, price:248, img:'https://images.unsplash.com/photo-1572804013309-59a88b7e92f1?w=80&q=60'},{name:'Solene Knot Top',color:'Ivory',size:'S',qty:1,price:164,img:'https://images.unsplash.com/photo-1515886657613-9f3515b0c78f?w=80&q=60'}] },
{ id:'1206', customer:'Sophie Tremblay', email:'sophie.t@email.com', total:184, status:'shipped', items:1, date:'2026-06-04 · 09:47', country:'CA', address:'220 Laurier Ave W, Ottawa ON K1P 5Z9, Canada', trackingNumber:'DHL-4928374651', notes:'', lineItems:[{name:'Mireille Pleated Skirt', color:'Blush', size:'XS', qty:1, price:184, img:'https://images.unsplash.com/photo-1539109136881-3be0616acf4b?w=80&q=60'}] },
{ id:'1205', customer:'Camille Roy', email:'camille.r@email.com', total:325, status:'fulfilled', items:1, date:'2026-06-03', country:'FR', address:'8 Rue du Faubourg Saint-Antoine, 75011 Paris', trackingNumber:'DHL-3817263540', notes:'Gift wrap requested.', lineItems:[{name:'Margaux Tailored Blazer', color:'Cocoa', size:'M', qty:1, price:325, img:'https://images.unsplash.com/photo-1515886657613-9f3515b0c78f?w=80&q=60'}] },
{ id:'1204', customer:'Olivia Park', email:'olivia.p@email.com', total:498, status:'paid', items:2, date:'2026-06-03', country:'CA', address:'1020 Burrard St, Vancouver BC V6Z 1Y5, Canada', trackingNumber:'', notes:'', lineItems:[{name:'Eloise Satin Slip', color:'Ivory', size:'S', qty:1, price:268, img:'https://images.unsplash.com/photo-1539109136881-3be0616acf4b?w=80&q=60'},{name:'Romy Cropped Cardigan',color:'Blush',size:'XS',qty:1,price:230,img:'https://images.unsplash.com/photo-1572804013309-59a88b7e92f1?w=80&q=60'}] },
{ id:'1203', customer:'Yasmine Khaled', email:'yasmine.k@email.com', total:168, status:'refunded', items:1, date:'2026-06-02', country:'TN', address:'15 Rue Ibn Khaldoun, 3000 Sfax, Tunisia', trackingNumber:'DHL-2736451823', notes:'Return received 2026-06-01.', lineItems:[{name:'Romy Cropped Cardigan', color:'Champagne', size:'M', qty:1, price:168, img:'https://images.unsplash.com/photo-1515886657613-9f3515b0c78f?w=80&q=60'}] },
{ id:'1202', customer:'Anaïs Lemay', email:'anais.l@email.com', total:198, status:'shipped', items:1, date:'2026-06-02', country:'CA', address:'2100 Mackay St, Montréal QC H3G 2J1, Canada', trackingNumber:'DHL-1837465029', notes:'', lineItems:[{name:'Lila Wide-Leg Trousers', color:'Ivory', size:'S', qty:1, price:198, img:'https://images.unsplash.com/photo-1539109136881-3be0616acf4b?w=80&q=60'}] },
{ id:'1201', customer:'Margaux Petit', email:'margaux.p@email.com', total:368, status:'fulfilled', items:1, date:'2026-06-01', country:'FR', address:'3 Allée des Roses, 69001 Lyon, France', trackingNumber:'DHL-0928374651', notes:'', lineItems:[{name:'Adèle Bias Maxi Dress', color:'Blush', size:'S', qty:1, price:368, img:'https://images.unsplash.com/photo-1572804013309-59a88b7e92f1?w=80&q=60'}] },
{ id:'1200', customer:'Rim Ben Salah', email:'rim.bs@email.com', total:124, status:'fulfilled', items:1, date:'2026-06-01', country:'TN', address:'7 Rue de Marseille, 5000 Monastir, Tunisia', trackingNumber:'DHL-9182736450', notes:'', lineItems:[{name:'Solene Knot Top', color:'Cocoa', size:'XS', qty:1, price:124, img:'https://images.unsplash.com/photo-1515886657613-9f3515b0c78f?w=80&q=60'}] },
{ id:'1199', customer:'Juliette Marec', email:'juliette.m@email.com',total:295, status:'fulfilled', items:1, date:'2026-05-31', country:'FR', address:'18 Boulevard des Capucines, 75009 Paris, France', trackingNumber:'DHL-8273645019', notes:'', lineItems:[{name:'Celeste Linen Co-ord Set', color:'Champagne', size:'M', qty:1, price:295, img:'https://images.unsplash.com/photo-1539109136881-3be0616acf4b?w=80&q=60'}] },
];
const ADMIN_TICKETS = [
{ id:'T-309', customer:'Léa Bernard', email:'lea.b@email.com', subject:'Size exchange for Aurelie slip', status:'open', priority:'normal', date:'24 min ago', unread:true,
snippet:"Hi, I’d love to exchange the size S for M — the fit is beautiful but slightly small at the bust…",
messages:[{ id:1, from:'customer', author:'Léa Bernard', body:"Hi,\n\nI’d love to exchange the size S for M — the fit is beautiful but slightly small at the bust. Is it still possible to do an exchange?\n\nBest,\nLéa", date:'24 min ago' }] },
{ id:'T-308', customer:'Sophie Tremblay', email:'sophie.t@email.com', subject:'When will my order ship?', status:'replied', priority:'normal', date:'1 h ago', unread:false,
snippet:"Hello, my order #1206 was placed two days ago and hasn’t shipped yet…",
messages:[
{ id:1, from:'customer', author:'Sophie Tremblay', body:"Hello,\n\nMy order #1206 was placed two days ago and hasn’t shipped yet — do you have an estimated dispatch date?\n\nThanks,\nSophie", date:'3 h ago' },
{ id:2, from:'team', author:'House of Sheylas', body:"Hi Sophie,\n\nYour order has been picked and is being packed. You’ll receive a DHL tracking confirmation within 24 hours.\n\nWarmly,\nThe HOS Team", date:'1 h ago' }
]},
{ id:'T-307', customer:'Olivia Park', email:'olivia.p@email.com', subject:'Combine two orders into one parcel?', status:'open', priority:'low', date:'3 h ago', unread:true,
snippet:"Hi team, I placed two orders this morning and was wondering if you could ship them together…",
messages:[{ id:1, from:'customer', author:'Olivia Park', body:"Hi team,\n\nI placed two orders this morning and was wondering if you could ship them together to save on packaging?\n\nThanks!\nOlivia", date:'3 h ago' }] },
{ id:'T-306', customer:'Yasmine Khaled', email:'yasmine.k@email.com',subject:'Refund question', status:'open', priority:'high', date:'Yesterday', unread:true,
snippet:"Hello, I returned the Romy cardigan a week ago — when can I expect the refund…",
messages:[{ id:1, from:'customer', author:'Yasmine Khaled', body:"Hello,\n\nI returned the Romy cardigan a week ago — the carrier confirmed delivery on May 28. When can I expect the refund to appear on my card?\n\nBest,\nYasmine", date:'Yesterday · 09:14' }] },
{ id:'T-305', customer:'Press @ Vogue', email:'press@vogue.fr', subject:'Press loan request — Spring Bloom', status:'replied', priority:'high', date:'Yesterday', unread:false,
snippet:"Bonjour, we’d love to feature pieces from the Spring Bloom Édition in our April issue…",
messages:[
{ id:1, from:'customer', author:'Vogue Press Team', body:"Bonjour,\n\nWe’d love to feature pieces from the Spring Bloom Édition in our April issue. Could we arrange a press loan of 3–5 looks for a shoot on June 12?\n\nMerci,\nVogue FR", date:'Yesterday · 11:30' },
{ id:2, from:'team', author:'House of Sheylas', body:"Bonjour,\n\nThank you — we’d be delighted to collaborate! Our PR team will follow up with loan agreement details by EOD.\n\nÀ bientôt,\nThe HOS Team", date:'Yesterday · 14:20' }
]},
{ id:'T-304', customer:'Rim Ben Salah', email:'rim.bs@email.com', subject:'Wholesale enquiry', status:'closed', priority:'normal', date:'2 days ago', unread:false,
snippet:"Bonjour, je tiens une concept-store à Tunis — j’aimerais discuter d’une collaboration…",
messages:[
{ id:1, from:'customer', author:'Rim Ben Salah', body:"Bonjour,\n\nJe tiens une concept-store à Tunis — j’aimerais discuter d’une collaboration wholesale. Pouvez-vous partager vos conditions?\n\nCordialement,\nRim", date:'2 days ago · 10:00' },
{ id:2, from:'team', author:'House of Sheylas', body:"Bonjour Rim,\n\nMerci pour votre intérêt! Notre équipe wholesale va vous contacter avec notre catalogue B2B et nos conditions.\n\nÀ très bientôt,\nHOS", date:'2 days ago · 15:00' }
]},
{ id:'T-303', customer:'Anaïs Lemay', email:'anais.l@email.com', subject:'Pricing in CAD', status:'replied', priority:'low', date:'2 days ago', unread:false,
snippet:"Hello, do you offer prices in Canadian dollars for shipments to Canada?",
messages:[
{ id:1, from:'customer', author:'Anaïs Lemay', body:"Hello, do you offer prices in Canadian dollars for shipments to Canada? The USD conversion makes it a bit tricky to budget.", date:'2 days ago · 08:30' },
{ id:2, from:'team', author:'House of Sheylas', body:"Hi Anaïs,\n\nAt the moment we invoice in USD, but your bank will convert at the daily rate. We’re exploring multi-currency support for later this year!\n\nThank you,\nHOS", date:'2 days ago · 11:00' }
]},
];
const ADMIN_TOP_PRODUCTS = [
{ id:'bs2', name:'Adèle Bias Maxi Dress', sold:48, revenue:48*368 },
{ id:'na1', name:'Aurelie Silk Slip Dress', sold:42, revenue:42*248 },
{ id:'bs1', name:'Margaux Tailored Blazer', sold:31, revenue:31*325 },
{ id:'na4', name:'Sienna Wrap Blouse', sold:28, revenue:28*148 },
{ id:'bs3', name:'Camille Cashmere Knit', sold:24, revenue:24*285 },
];
const ADMIN_CHANNELS = [
{ name:'Direct', pct:46, color:'#B86A78' },
{ name:'Instagram',pct:24, color:'#4A2D33' },
{ name:'Search', pct:18, color:'#F6D6DA' },
{ name:'Other', pct:12, color:'#F2E3DC' },
];
const ADMIN_COUNTRIES = [
{ code:'FR', name:'France', pct:42 },
{ code:'CA', name:'Canada', pct:31 },
{ code:'TN', name:'Tunisia', pct:14 },
{ code:'US', name:'United States', pct:8 },
{ code:'·', name:'Other', pct:5 },
];
/* ───────────── Admin state context ───────────── */
const AdminCtx = createContext(null);
function AdminProvider({ children }) {
const { siteData, updateSiteData, resetSiteData } = useSiteData();
const products = siteData.products;
const categories = siteData.categories.map((category) => ({
...category,
productCount: siteData.products.filter((product) => product.category === category.id).length,
}));
const [orders, setOrders] = useState(ADMIN_ORDERS);
const [tickets, setTickets] = useState(ADMIN_TICKETS);
const [toast, setToast] = useState(null);
const flash = (msg, tone='success') => {
setToast({ msg, tone, ts: Date.now() });
setTimeout(() => setToast(null), 2400);
};
const saveProduct = (p) => {
updateSiteData((current) => {
const nextProducts = [...current.products];
const index = nextProducts.findIndex((item) => item.id === p.id);
if (index >= 0) {
nextProducts[index] = { ...nextProducts[index], ...p };
} else {
nextProducts.unshift({ ...p, id: p.id || `new-${Date.now()}` });
}
return { ...current, products: nextProducts };
});
flash('Product saved');
};
const deleteProduct = (id) => {
updateSiteData((current) => ({
...current,
products: current.products.filter((product) => product.id !== id),
}));
flash('Product deleted','warn');
};
const adjustStock = (id, delta) => {
updateSiteData((current) => ({
...current,
products: current.products.map((product) =>
product.id === id
? { ...product, stock: Math.max(0, (product.stock || 0) + delta) }
: product
),
}));
};
const setStock = (id, value) => {
updateSiteData((current) => ({
...current,
products: current.products.map((product) =>
product.id === id ? { ...product, stock: Math.max(0, value) } : product
),
}));
flash('Stock updated');
};
const restock = (id, amount = 40) => {
updateSiteData((current) => ({
...current,
products: current.products.map((product) =>
product.id === id ? { ...product, stock: (product.stock || 0) + amount } : product
),
}));
flash(`Restocked +${amount}`);
};
const saveCategory = (c) => {
updateSiteData((current) => {
const nextCategories = [...current.categories];
const index = nextCategories.findIndex((item) => item.id === c.id);
const category = {
...c,
id: c.id || `cat-${Date.now()}`,
subcategories: c.subcategories || [],
};
if (index >= 0) nextCategories[index] = { ...nextCategories[index], ...category };
else nextCategories.unshift(category);
return { ...current, categories: nextCategories };
});
flash('Category saved');
};
const deleteCategory = (id) => {
updateSiteData((current) => ({
...current,
categories: current.categories.filter((category) => category.id !== id),
}));
flash('Category deleted','warn');
};
const savePages = (pages) => {
updateSiteData((current) => ({ ...current, pages }));
flash('Pages saved');
};
const saveContent = (key, value) => {
updateSiteData((current) => ({ ...current, [key]: value }));
flash('Content saved');
};
const saveSettings = (settings) => {
updateSiteData((current) => ({ ...current, settings: { ...current.settings, ...settings } }));
flash('Settings saved');
};
const updateOrderStatus = (id, status) => { setOrders(c => c.map(o => o.id === id ? {...o, status} : o)); flash('Order updated'); };
const updateOrderField = (id, patch) => { setOrders(c => c.map(o => o.id === id ? {...o, ...patch} : o)); };
const updateTicket = (id, patch) => { setTickets(c => c.map(t => t.id === id ? {...t, ...patch} : t)); };
const addTicketMessage = (id, msg) => {
setTickets(c => c.map(t => t.id === id
? { ...t, messages: [...(t.messages || []), msg], status: 'replied', unread: false }
: t
));
flash('Reply sent');
};
return (
{children}
);
}
const useAdmin = () => useContext(AdminCtx);
/* ───────────── Common admin pieces ───────────── */
function StatusPill({ status }) {
const map = {
paid: { bg:'bg-blush', text:'text-deep', label:'Paid' },
shipped: { bg:'bg-champagne', text:'text-deep', label:'Shipped' },
fulfilled: { bg:'bg-rose/15', text:'text-rose', label:'Fulfilled' },
refunded: { bg:'bg-deep/10', text:'text-deep/70', label:'Refunded' },
live: { bg:'bg-rose/15', text:'text-rose', label:'Live' },
draft: { bg:'bg-deep/10', text:'text-deep/70', label:'Draft' },
open: { bg:'bg-rose', text:'text-ivory', label:'Open' },
replied: { bg:'bg-champagne', text:'text-deep', label:'Replied' },
closed: { bg:'bg-deep/10', text:'text-deep/60', label:'Closed' },
high: { bg:'bg-rose', text:'text-ivory', label:'High' },
normal: { bg:'bg-deep/10', text:'text-deep/70', label:'Normal' },
low: { bg:'bg-blush-2', text:'text-deep/70', label:'Low' },
};
const m = map[status] || { bg:'bg-deep/10', text:'text-deep/70', label:status };
return (
{m.label}
);
}
function AdminCard({ children, className = '', accent = false }) {
return (
{children}
);
}
function AdminToast() {
const a = useAdmin();
if (!a?.toast) return null;
return (
);
}
/* ───────────── Charts ───────────── */
function SalesChart({ data }) {
const W = 720, H = 220, P = 28;
const max = Math.max(...data.map(d => d.value)) * 1.15;
const min = 0;
const x = (i) => P + (i / (data.length - 1)) * (W - P*2);
const y = (v) => H - P - ((v - min) / (max - min)) * (H - P*2);
const points = data.map((d, i) => [x(i), y(d.value)]);
const linePath = points.map((p, i) => `${i===0?'M':'L'} ${p[0]} ${p[1]}`).join(' ');
const areaPath = `${linePath} L ${points[points.length-1][0]} ${H-P} L ${points[0][0]} ${H-P} Z`;
const [hover, setHover] = useState(null);
const days = data.map(d => d.date);
const dayLabels = [0, 7, 14, 21, 29].map(i => ({
x: x(i),
label: days[i] ? days[i].toLocaleDateString('en', { day:'numeric', month:'short' }) : '',
}));
return (
{hover !== null && (
{data[hover].date.toLocaleDateString('en', { weekday:'short', day:'numeric', month:'short' })}
${data[hover].value.toLocaleString()}
)}
);
}
function DonutChart({ segments, size = 140 }) {
const r = size/2 - 18;
const c = 2 * Math.PI * r;
let offset = 0;
return (
);
}
/* ───────────── Admin shell ───────────── */
function AdminShell({ children, active }) {
const a = useAdmin();
const auth = useAdminAuth();
const [open, setOpen] = useState(false); // mobile sidebar
const links = [
{ id:'home', label:'Dashboard', href:'#admin', icon: AdminIcons.dash },
{ id:'products', label:'Products', href:'#admin/products', icon: AdminIcons.bag, count: a.products.length },
{ id:'categories', label:'Categories', href:'#admin/categories',icon: AdminIcons.grid, count: a.categories.length },
{ id:'content', label:'Content', href:'#admin/content', icon: AdminIcons.edit, count: a.pages.length },
{ id:'inventory', label:'Inventory', href:'#admin/inventory', icon: AdminIcons.layers, count: a.products.filter(p => (p.stock||0) < 10).length, dot: a.products.some(p => (p.stock||0) < 10) },
{ id:'orders', label:'Orders', href:'#admin/orders', icon: AdminIcons.box, count: a.orders.length },
{ id:'tickets', label:'Tickets', href:'#admin/tickets', icon: AdminIcons.chat, count: a.tickets.filter(t => t.unread).length, dot:true },
{ id:'analytics', label:'Analytics', href:'#admin/analytics', icon: AdminIcons.chart },
{ id:'settings', label:'Settings', href:'#admin/settings', icon: AdminIcons.cog },
];
return (
{/* Sidebar */}
{/* Overlay for mobile */}
{open &&
setOpen(false)}/>}
{/* Main */}
{/* Top bar */}
House of Sheylas · Admin
{children}
);
}
/* ───────────── Icons (admin-specific minimal set) ───────────── */
const AdminIcons = {
dash: (p) =>
,
bag: (p) =>
,
grid: (p) =>
,
box: (p) =>
,
chat: (p) =>
,
chart: (p) =>
,
cog: (p) =>
,
bell: (p) =>
,
exit: (p) =>
,
trash: (p) =>
,
edit: (p) =>
,
dots: (p) =>
,
upload:(p) =>
,
filter:(p) =>
,
layers:(p) =>
,
alert: (p) =>
,
trend: (p) =>
,
};
/* ───────────── Page: Dashboard ───────────── */
function AdminDashboard() {
const a = useAdmin();
return (
{/* KPIs */}
{ADMIN_KPIS.map(k => (
{k.label}
{k.value}
= 0 ? 'text-rose' : 'text-deep/60'}`}>
{k.delta >= 0 ? '↑' : '↓'}
{Math.abs(k.delta)}%
vs previous
))}
{/* Sales chart + channels */}
Revenue · Last 30 days
${ADMIN_SALES_30D.reduce((s,d)=>s+d.value,0).toLocaleString()}
{['7d','30d','90d','12m'].map((r, i) => (
))}
{/* Channels donut */}
Traffic sources
{ADMIN_CHANNELS.map(c => (
-
{c.name}
{c.pct}%
))}
{/* Top products + recent orders + tickets */}
{/* Top products */}
{/* Recent orders */}
| Order |
Customer |
Date |
Status |
Total |
{a.orders.slice(0, 6).map(o => (
|
#{o.id}
|
{o.customer} |
{o.date} |
|
${o.total} |
))}
{/* Tickets + Geography */}
{a.tickets.slice(0, 4).map(t => (
-
))}
By country
{ADMIN_COUNTRIES.map(c => (
-
{c.name}
{c.pct}%
{/* Inventory health */}
{(() => {
const units = a.products.reduce((s,p) => s + (p.stock||0), 0);
const value = a.products.reduce((s,p) => s + (p.stock||0)*(p.price||0), 0);
const low = a.products.filter(p => (p.stock||0) > 0 && (p.stock||0) < 10);
const out = a.products.filter(p => (p.stock||0) === 0).length;
return (
{[
['Units in stock', units.toLocaleString()],
['Inventory value', `$${value.toLocaleString()}`],
['Low stock', low.length, low.length>0],
['Out of stock', out, out>0],
].map(([label, val, warn]) => (
))}
{low.length > 0 && (
)}
);
})()}
);
}
function PageHeading({ title, sub, actions }) {
return (
);
}
/* ───────────── Page: Products list ───────────── */
function AdminProductsPage() {
const a = useAdmin();
const [filter, setFilter] = useState('all');
const [q, setQ] = useState('');
const rows = useMemo(() => {
let arr = a.products;
if (filter !== 'all') arr = arr.filter(p => p.status === filter);
if (q) arr = arr.filter(p => p.name.toLowerCase().includes(q.toLowerCase()));
return arr;
}, [a.products, filter, q]);
return (
Add product
}
/>
{/* Toolbar */}
{/* Table */}
| Product |
Category |
Price |
Stock |
Status |
Actions |
{rows.map(p => (
|
|
{p.category} |
${p.price} |
{p.stock}
{p.stock < 10 && Low}
|
|
|
))}
{rows.length === 0 && (
Nothing here
No products match your filters.
)}
{rows.length} of {a.products.length} pieces
1 / 1
);
}
/* ───────────── Variant / media editors ───────────── */
function ColorComposer({ onAdd }) {
const [duo, setDuo] = useState(false);
const [a, setA] = useState('#B86A78');
const [b, setB] = useState('#2A4C7D');
const [name, setName] = useState('');
const add = () => {
const color = duo
? { name: name.trim() || 'Two-tone', hex: a, hex2: b }
: { name: name.trim() || a.toUpperCase(), hex: a };
onAdd(color);
setName('');
};
const preview = { background: duo ? `linear-gradient(135deg, ${a} 0 50%, ${b} 50% 100%)` : a };
return (
);
}
function TagEditor({ tags, onChange }) {
const [input, setInput] = useState('');
const presets = ['New','Just in','Limited','Bestseller','Most loved','Restocked','Back soon','Exclusive'];
const addTag = (t) => {
const v = (t || '').trim();
if (!v || tags.some(x => x.toLowerCase() === v.toLowerCase())) { setInput(''); return; }
onChange([...tags, v]); setInput('');
};
return (
{tags.length > 0 && (
{tags.map((t, i) => (
{i === 0 && }
{t}
))}
)}
setInput(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); addTag(input); } }}
placeholder="Type a tag, press Enter…" className="admin-input"/>
{presets.filter(p => !tags.some(t => t.toLowerCase() === p.toLowerCase())).map(p => (
))}
The first tag shows as the badge on the storefront.
);
}
function ImageUploader({ value, onChange }) {
const inputRef = useRef(null);
const [drag, setDrag] = useState(false);
const isData = typeof value === 'string' && value.startsWith('data:');
const readFile = (file) => {
if (!file || !file.type.startsWith('image/')) return;
const fr = new FileReader();
fr.onload = () => onChange(fr.result);
fr.readAsDataURL(file);
};
return (
<>
inputRef.current && inputRef.current.click()}
onDragOver={e => { e.preventDefault(); setDrag(true); }}
onDragLeave={() => setDrag(false)}
onDrop={e => { e.preventDefault(); setDrag(false); readFile(e.dataTransfer.files[0]); }}
className={`group mt-3 relative aspect-[3/4] overflow-hidden bg-blush-2 cursor-pointer transition-all
${drag ? 'ring-2 ring-rose ring-offset-2 ring-offset-ivory' : ''}`}>
{value &&

}
{drag ? 'Drop to upload' : value ? 'Replace image' : 'Click or drop image'}
PNG · JPG · WEBP
readFile(e.target.files[0])}/>
onChange(e.target.value)} className="admin-input mt-3" placeholder="…or paste an image URL"/>
{isData &&
Uploaded file attached ✓
}
>
);
}
/* ───────────── Page: Product form (new / edit) ───────────── */
function AdminProductFormPage({ productId }) {
const a = useAdmin();
const editing = productId && productId !== 'new';
const existing = editing ? a.products.find(p => p.id === productId) : null;
const [form, setForm] = useState(() => {
if (existing) return { ...existing, tags: existing.tags || (existing.tag ? [existing.tag] : []) };
return {
name: '', price: 0, category: 'dresses', stock: 20, status: 'draft',
description: '', img: IMG('photo-1539109136881-3be0616acf4b', 700),
colors: [SW.blush, SW.ivory], tags: ['New'], tag: 'New',
};
});
const set = (k, v) => setForm(f => ({...f, [k]: v}));
const setTags = (tags) => setForm(f => ({...f, tags, tag: tags[0] || ''}));
const onSave = (e) => {
e.preventDefault();
a.saveProduct(form);
window.location.hash = '#admin/products';
};
return (
);
}
function SectionTitle({ children }) {
return
{children}
;
}
function Field({ label, children, className = '' }) {
return (
);
}
/* ───────────── Page: Categories ───────────── */
function AdminCategoriesPage() {
const a = useAdmin();
const [editing, setEditing] = useState(null); // category being edited (or 'new')
return (
setEditing({ id: '', name: '', count: '0 pieces', img: IMG('photo-1572804013309-59a88b7e92f1', 700), status: 'live' })}
className="btn-sweep inline-flex items-center gap-2 bg-deep text-ivory font-sans text-[11px] tracking-widest-2 uppercase px-4 py-2.5 hover:bg-rose transition-colors">
New category
}
/>
{a.categories.map(c => (
{c.name}
{c.productCount} pieces
))}
{editing && setEditing(null)} onSave={(c) => { a.saveCategory(c); setEditing(null); }}/>}
);
}
function CategoryDrawer({ cat, onClose, onSave }) {
const [form, setForm] = useState({...cat});
const set = (k, v) => setForm(f => ({...f, [k]: v}));
const subcategoryValue = (form.subcategories || []).map((item) => item.name).join(', ');
return (
);
}
/* ───────────── Page: Orders list ───────────── */
const ORDERS_PAGE_SIZE = 8;
function AdminOrdersPage() {
const a = useAdmin();
const [statusF, setStatusF] = useState('all');
const [q, setQ] = useState('');
const [sort, setSort] = useState({ col:'date', dir:'desc' });
const [page, setPage] = useState(1);
const toggleSort = (col) => {
setSort(s => s.col === col ? { col, dir: s.dir === 'asc' ? 'desc' : 'asc' } : { col, dir: 'desc' });
setPage(1);
};
const filtered = useMemo(() => {
let arr = [...a.orders];
if (statusF !== 'all') arr = arr.filter(o => o.status === statusF);
if (q.trim()) arr = arr.filter(o =>
o.customer.toLowerCase().includes(q.toLowerCase()) ||
o.email.toLowerCase().includes(q.toLowerCase()) ||
o.id.includes(q.replace('#',''))
);
const { col, dir } = sort;
arr.sort((x, y) => {
const av = x[col], bv = y[col];
if (col === 'total') return dir === 'asc' ? av - bv : bv - av;
return dir === 'asc' ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av));
});
return arr;
}, [a.orders, statusF, q, sort]);
const totalPages = Math.max(1, Math.ceil(filtered.length / ORDERS_PAGE_SIZE));
const rows = filtered.slice((page - 1) * ORDERS_PAGE_SIZE, page * ORDERS_PAGE_SIZE);
const exportCSV = () => {
const head = ['Order','Customer','Email','Country','Date','Items','Status','Total'];
const lines = [head.join(','), ...a.orders.map(o =>
[`#${o.id}`, `"${o.customer}"`, o.email, o.country, `"${o.date}"`, o.items, o.status, o.total].join(',')
)];
const blob = new Blob([lines.join('\n')], { type:'text/csv' });
const url = URL.createObjectURL(blob);
const el = document.createElement('a'); el.href = url; el.download = 'orders.csv'; el.click();
URL.revokeObjectURL(url);
};
const SortTh = ({ col, children, right }) => {
const active = sort.col === col;
return (
toggleSort(col)}
className={`py-3 px-4 font-medium cursor-pointer select-none hover:text-deep transition-colors ${right ? 'text-right' : ''}`}>
{children}
{sort.dir === 'asc' ? '↑' : '↓'}
|
);
};
return (
Export CSV
}
/>
{/* KPI cards — click to filter */}
{(['paid','shipped','fulfilled','refunded']).map(s => {
const subset = a.orders.filter(o => o.status === s);
const rev = subset.reduce((acc, o) => acc + o.total, 0);
const active = statusF === s;
return (
{ setStatusF(f => f === s ? 'all' : s); setPage(1); }}>
{subset.length}
${rev.toLocaleString()}
);
})}
{/* Toolbar */}
{[['all','All'],['paid','Paid'],['shipped','Shipped'],['fulfilled','Fulfilled'],['refunded','Refunded']].map(([k, l]) => (
))}
{ setQ(e.target.value); setPage(1); }}
placeholder="Customer, email, or #order…"
className="w-full bg-blush-2/40 border border-transparent hover:border-deep/15 focus:border-deep focus:bg-ivory focus:outline-none transition-all pl-9 pr-3 py-2 font-sans text-[12px] tracking-wider-2 text-deep placeholder:text-deep/40"/>
{/* Table */}
Order
Customer
| Country |
Date
Items |
Status
Total
{rows.map(o => (
window.location.hash = `#admin/orders/${o.id}`}>
|
#{o.id}
|
{o.customer}
{o.email}
|
{o.country} |
{o.date} |
{o.items} |
|
${o.total.toLocaleString()}
|
))}
{rows.length === 0 && (
No orders found
Adjust your filters or search term.
)}
{/* Pagination + summary */}
{filtered.length} order{filtered.length !== 1 ? 's' : ''}
{filtered.length > 0 && <> · ${filtered.reduce((s,o)=>s+o.total,0).toLocaleString()} total>}
Page {page} / {totalPages}
);
}
/* ───────────── Page: Order detail ───────────── */
function AdminOrderDetailPage({ orderId }) {
const a = useAdmin();
const o = a.orders.find(x => x.id === orderId) || a.orders[0];
const [tracking, setTracking] = useState(o?.trackingNumber || '');
const [notes, setNotes] = useState(o?.notes || '');
const [noteSaved, setNoteSaved] = useState(false);
useEffect(() => {
setTracking(o?.trackingNumber || '');
setNotes(o?.notes || '');
setNoteSaved(false);
}, [o?.id]);
const statuses = ['paid','shipped','fulfilled'];
const reached = statuses.indexOf(o.status === 'refunded' ? 'paid' : o.status);
const shippingCost = o.total > 200 ? 0 : 12;
const saveTracking = () => { a.updateOrderField(o.id, { trackingNumber: tracking }); a.flash('Tracking saved'); };
const saveNotes = () => {
a.updateOrderField(o.id, { notes });
setNoteSaved(true);
setTimeout(() => setNoteSaved(false), 2200);
};
const customerOrders = a.orders.filter(x => x.email === o.email);
const customerLifetime = customerOrders.reduce((s, x) => s + x.total, 0);
return (
{/* Breadcrumb */}
>
}
/>
{/* Left — items, timeline, tracking */}
{/* Line items */}
Items ({o.items})
{[
['Subtotal', `$${(o.total - shippingCost).toLocaleString()}`],
['Shipping', shippingCost === 0 ? 'Free' : `$${shippingCost}`],
['Tax', '$0'],
].map(([l, v]) => (
{l}{v}
))}
Total
${o.total.toLocaleString()}
{/* Fulfillment timeline — reacts to status dropdown */}
Fulfillment timeline
{[
{ key:'paid', label:'Payment received', sub: o.date },
{ key:'shipped', label:'Parcel handed to carrier', sub: o.trackingNumber ? `Tracking: ${o.trackingNumber}` : 'No tracking yet' },
{ key:'fulfilled', label:'Delivered to customer', sub: '—' },
].map(({ key, label, sub }, i) => {
const done = i <= reached && o.status !== 'refunded';
return (
-
{done ? : {i+1}}
{label}
{done ? sub : 'Pending'}
);
})}
{o.status === 'refunded' && (
-
)}
{/* Tracking number */}
Tracking number
setTracking(e.target.value)}
placeholder="e.g. DHL-4928374651"
className="admin-input"
onKeyDown={e => e.key === 'Enter' && saveTracking()}/>
{o.trackingNumber && (
Active: {o.trackingNumber}
)}
{/* Right sidebar */}
{/* Customer card — real lifetime from orders data */}
Customer
{customerOrders.length}
Orders
${customerLifetime.toLocaleString()}
Lifetime
Email customer
{/* Shipping address */}
Shipping address
{o.customer}{'\n'}{o.address || '—'}
Method: {o.trackingNumber ? 'DHL Express' : 'Standard shipping'}
{/* Internal notes — saves to order state */}
Internal notes
);
}
/* ───────────── Page: Tickets (inbox) ───────────── */
function AdminTicketsPage({ ticketId }) {
const a = useAdmin();
const [statusF, setStatusF] = useState('all');
const [searchQ, setSearchQ] = useState('');
const [replyMode, setReplyMode] = useState('reply'); // 'reply' | 'note'
const [reply, setReply] = useState('');
const filtered = useMemo(() => {
let arr = a.tickets;
if (statusF !== 'all') arr = arr.filter(t => t.status === statusF);
if (searchQ.trim()) arr = arr.filter(t =>
t.customer.toLowerCase().includes(searchQ.toLowerCase()) ||
t.subject.toLowerCase().includes(searchQ.toLowerCase()) ||
(t.email || '').toLowerCase().includes(searchQ.toLowerCase())
);
return arr;
}, [a.tickets, statusF, searchQ]);
const selected = a.tickets.find(t => t.id === ticketId) || filtered[0] || a.tickets[0];
useEffect(() => {
if (selected?.unread) a.updateTicket(selected.id, { unread: false });
setReply('');
setReplyMode('reply');
}, [selected?.id]);
const send = () => {
if (!reply.trim()) return;
const msg = {
id: Date.now(),
from: replyMode === 'note' ? 'note' : 'team',
author: replyMode === 'note' ? 'Internal note' : 'House of Sheylas',
body: reply.trim(),
date: 'Just now',
};
if (replyMode === 'note') {
a.updateTicket(selected.id, { messages: [...(selected.messages || []), msg] });
a.flash('Note added');
} else {
a.addTicketMessage(selected.id, msg);
}
setReply('');
};
const unreadCount = a.tickets.filter(t => t.unread).length;
const openCount = a.tickets.filter(t => t.status === 'open').length;
return (
{/* Left — ticket list */}
{/* Status filters */}
{[['all','All'],['open','Open'],['replied','Replied'],['closed','Closed']].map(([k, l]) => (
))}
{/* Search */}
{/* List */}
{/* Right — conversation thread */}
{selected ? (
<>
{/* Thread header */}
{selected.subject}
{selected.customer}
{selected.email && <> ·
{selected.email}>}
{' · '}#{selected.id} · {selected.date}
{/* Messages */}
{(selected.messages || []).map((msg, i) => {
const isNote = msg.from === 'note';
const isTeam = msg.from === 'team';
return (
{!isTeam && (
{isNote ? '✎' : selected.customer.charAt(0)}
)}
{msg.author}
· {msg.date}
{isNote && (
Note
)}
{msg.body}
{isTeam && (
S
)}
);
})}
{/* Composer */}
{selected.status !== 'closed' ? (
{/* Mode tabs */}
{[['reply','Reply to customer'],['note','Internal note']].map(([k, l]) => (
))}
) : (
This ticket is closed.
)}
>
) : (
Inbox zero.
Nothing here right now.
)}
);
}
/* ───────────── Page: Analytics ───────────── */
function AdminAnalyticsPage() {
// simple bar chart for top categories revenue
const catRevenue = useMemo(() => {
const m = {};
ADMIN_TOP_PRODUCTS.forEach(p => {
const prod = ALL_PRODUCTS.find(x => x.id === p.id);
const cat = prod?.category || 'misc';
m[cat] = (m[cat] || 0) + p.revenue;
});
// include some baseline for other cats
['dresses','tops','sets','bottoms','accessories'].forEach(c => { if (!m[c]) m[c] = 2400 + Math.random()*4000; });
const arr = Object.entries(m).map(([cat, rev]) => ({ cat, rev: Math.round(rev) })).sort((a,b) => b.rev - a.rev);
return arr;
}, []);
const maxRev = Math.max(...catRevenue.map(c => c.rev));
return (
Revenue · Last 30 days
Traffic sources
{ADMIN_CHANNELS.map(c => (
-
{c.name}
{c.pct}%
))}
Revenue by category
{catRevenue.map(c => (
-
{c.cat}
${c.rev.toLocaleString()}
))}
By country
{ADMIN_COUNTRIES.map(c => (
-
{c.code}
);
}
/* ───────────── Page: Settings ───────────── */
function AdminContentPage() {
const a = useAdmin();
const [pages, setPages] = useState(a.pages);
const [hero, setHero] = useState(a.hero);
const [story, setStory] = useState(a.story);
const [about, setAbout] = useState(a.about);
const [contact, setContact] = useState(a.contact);
const [services, setServices] = useState(a.services.map((service) => service.title).join(', '));
return (
Pages
Hero
setHero({ ...hero, eyebrow: e.target.value })} className="admin-input" />
setHero({ ...hero, title: e.target.value })} className="admin-input" />
setHero({ ...hero, accent: e.target.value })} className="admin-input" />
Brand story
setStory({ ...story, title: e.target.value })} className="admin-input" />
setStory({ ...story, accent: e.target.value })} className="admin-input" />
setStory({ ...story, end: e.target.value })} className="admin-input" />
setStory({ ...story, imagePrimary: e.target.value })} className="admin-input" />
setStory({ ...story, imageSecondary: e.target.value })} className="admin-input" />
About page
setAbout({ ...about, eyebrow: e.target.value })} className="admin-input" />
setAbout({ ...about, title: e.target.value })} className="admin-input" />
setAbout({ ...about, accent: e.target.value })} className="admin-input" />
Contact & services
setContact({ ...contact, responseTime: e.target.value })} className="admin-input" />
);
}
function AdminSettingsPage() {
const a = useAdmin();
const [form, setForm] = useState({
brandName: a.settings.brandName || '',
brandMark: a.settings.brandMark || '',
brandTagline: a.settings.brandTagline || '',
contact: a.settings.supportEmail || '',
pressEmail: a.settings.pressEmail || '',
phone: a.settings.phone || '',
currency: a.settings.currency || 'USD',
welcomeCode: a.settings.newsletterCode || '',
replyTo: a.settings.newsletterReplyTo || '',
autoReply: a.settings.autoReply || 'Thank you for writing to us. We typically reply within 48 hours.',
instagramHandle: a.settings.instagramHandle || '',
instagramUrl: a.settings.instagramUrl || '',
tiktokUrl: a.settings.tiktokUrl || '',
pinterestUrl: a.settings.pinterestUrl || '',
adminUsername: a.settings.adminUsername || '',
adminPassword: a.settings.adminPassword || '',
});
const set = (k, v) => setForm(f => ({...f, [k]: v}));
const [showPw, setShowPw] = useState(false);
const defaultLocations = [
{ id:'loc-1', city:'Paris', role:'Studio', note:'' },
{ id:'loc-2', city:'Canada', role:'Operations', note:'' },
{ id:'loc-3', city:'Tunisia', role:'Atelier', note:'' },
];
const [locations, setLocations] = useState(
(a.locations && a.locations.length > 0) ? a.locations : defaultLocations
);
const updateLoc = (i, patch) => setLocations(ls => ls.map((l, idx) => idx === i ? {...l, ...patch} : l));
const addLoc = () => setLocations(ls => [...ls, { id:`loc-${Date.now()}`, city:'', role:'', note:'' }]);
const removeLoc = (i) => setLocations(ls => ls.filter((_, idx) => idx !== i));
const saveAll = () => {
a.saveSettings({
brandName: form.brandName,
brandMark: form.brandMark,
brandTagline: form.brandTagline,
supportEmail: form.contact,
pressEmail: form.pressEmail,
phone: form.phone,
currency: form.currency,
newsletterCode: form.welcomeCode,
newsletterReplyTo: form.replyTo,
autoReply: form.autoReply,
instagramHandle: form.instagramHandle,
instagramUrl: form.instagramUrl,
tiktokUrl: form.tiktokUrl,
pinterestUrl: form.pinterestUrl,
adminUsername: form.adminUsername,
adminPassword: form.adminPassword,
});
a.saveContent('locations', locations);
};
return (
Save all settings
}
/>
{/* Brand */}
Brand
set('brandName', e.target.value)} className="admin-input"/>
set('brandMark', e.target.value)} className="admin-input"/>
set('brandTagline', e.target.value)} className="admin-input"/>
set('contact', e.target.value)} className="admin-input"/>
set('pressEmail', e.target.value)} className="admin-input"/>
set('phone', e.target.value)} className="admin-input"/>
{/* Social */}
Social & online presence
set('instagramHandle', e.target.value)} className="admin-input" placeholder="@roudaina"/>
set('instagramUrl', e.target.value)} className="admin-input" placeholder="https://instagram.com/…"/>
set('tiktokUrl', e.target.value)} className="admin-input" placeholder="https://tiktok.com/@…"/>
set('pinterestUrl', e.target.value)} className="admin-input" placeholder="https://pinterest.com/…"/>
{/* Newsletter */}
Newsletter
set('welcomeCode', e.target.value)} className="admin-input" placeholder="WELCOME10"/>
set('replyTo', e.target.value)} className="admin-input"/>
{/* Auto-reply */}
Customer auto-reply
{/* Locations — editable */}
Locations
{/* Admin access */}
Admin access
Credentials stored locally (localStorage). For production security use Supabase Auth at
/admin.
set('adminUsername', e.target.value)} className="admin-input"/>
);
}
/* ───────────── Page: Inventory ───────────── */
function stockState(n) {
if (!n || n <= 0) return 'out';
if (n < 10) return 'low';
if (n < 20) return 'fair';
return 'healthy';
}
function StockPill({ n }) {
const s = stockState(n);
const map = {
out: { bg:'bg-deep', text:'text-ivory', label:'Out of stock' },
low: { bg:'bg-rose', text:'text-ivory', label:'Low' },
fair: { bg:'bg-champagne', text:'text-deep', label:'Fair' },
healthy: { bg:'bg-rose/15', text:'text-rose', label:'In stock' },
};
const m = map[s];
return {m.label};
}
function StepperStock({ value, onMinus, onPlus, onSet }) {
return (
{ const v = parseInt(e.target.value.replace(/\D/g,''),10); onSet(Number.isFinite(v) ? v : 0); }}
className={`w-12 text-center font-sans text-[13px] tracking-wider-2 bg-transparent outline-none ${value < 10 ? 'text-rose' : 'text-deep'}`}
inputMode="numeric"
/>
);
}
function AdminInventoryPage() {
const a = useAdmin();
const [filter, setFilter] = useState('all'); // all | low | out | healthy
const [q, setQ] = useState('');
const [sort, setSort] = useState('stock'); // stock | value | name
const stats = useMemo(() => {
const units = a.products.reduce((s,p) => s + (p.stock||0), 0);
const value = a.products.reduce((s,p) => s + (p.stock||0) * (p.price||0), 0);
const low = a.products.filter(p => stockState(p.stock) === 'low').length;
const out = a.products.filter(p => stockState(p.stock) === 'out').length;
return { units, value, low, out };
}, [a.products]);
// Units in stock by category — simple bar viz
const byCat = useMemo(() => {
const m = {};
a.products.forEach(p => { m[p.category] = (m[p.category]||0) + (p.stock||0); });
const arr = Object.entries(m).map(([cat, units]) => ({ cat, units })).sort((x,y) => y.units - x.units);
const max = Math.max(1, ...arr.map(c => c.units));
return { arr, max };
}, [a.products]);
const lowList = useMemo(
() => a.products.filter(p => stockState(p.stock) === 'low' || stockState(p.stock) === 'out')
.sort((x,y) => (x.stock||0) - (y.stock||0)),
[a.products]
);
const rows = useMemo(() => {
let arr = [...a.products];
if (filter !== 'all') arr = arr.filter(p => stockState(p.stock) === filter);
if (q) arr = arr.filter(p => p.name.toLowerCase().includes(q.toLowerCase()));
arr.sort((x,y) => {
if (sort === 'value') return ((y.stock||0)*(y.price||0)) - ((x.stock||0)*(x.price||0));
if (sort === 'name') return x.name.localeCompare(y.name);
return (x.stock||0) - (y.stock||0); // low first
});
return arr;
}, [a.products, filter, q, sort]);
const restockAllLow = () => {
lowList.forEach(p => a.restock(p.id, 40));
};
return (
0 && (
)
}
/>
{/* KPIs */}
{[
{ label:'Units in stock', value: stats.units.toLocaleString(), foot:`${a.products.length} active SKUs` },
{ label:'Inventory value', value:`$${stats.value.toLocaleString()}`, foot:'At current retail' },
{ label:'Low stock', value: stats.low, foot:'Under 10 left', warn: stats.low > 0 },
{ label:'Out of stock', value: stats.out, foot:'Needs restock', warn: stats.out > 0 },
].map(k => (
{k.label}
{k.value}
{k.foot}
))}
{/* Low-stock alert + stock by category */}
{lowList.length} item{lowList.length===1?'':'s'}
{lowList.length === 0 ? (
All stocked up.
Nothing is running low right now.
) : (
{lowList.slice(0, 5).map(p => (
-
))}
)}
Units by category
{byCat.arr.map(c => (
-
{c.cat}
{c.units}
{/* Inventory table */}
{[['all','All'],['low','Low'],['out','Out'],['healthy','In stock']].map(([k, l]) => (
))}
| Product |
Category |
Price |
On hand |
Status |
Stock value |
Restock |
{rows.map(p => (
|
|
{p.category} |
${p.price} |
a.adjustStock(p.id, -1)}
onPlus={() => a.adjustStock(p.id, +1)}
onSet={(v) => a.setStock(p.id, v)}
/>
|
|
${((p.stock||0)*(p.price||0)).toLocaleString()} |
|
))}
{rows.length === 0 && (
Nothing here
No products match your filters.
)}
{rows.length} of {a.products.length} SKUs
{stats.units.toLocaleString()} units · ${stats.value.toLocaleString()} value
);
}
/* ───────────── Admin router ───────────── */
function AdminRouter({ subPath }) {
const [head, ...rest] = (subPath || '').split('/');
if (!head) return ;
if (head === 'products') {
if (rest[0] === 'new') return ;
if (rest[0]) return ;
return ;
}
if (head === 'categories') return ;
if (head === 'content') return ;
if (head === 'inventory') return ;
if (head === 'orders') {
if (rest[0]) return ;
return ;
}
if (head === 'tickets') return ;
if (head === 'analytics') return ;
if (head === 'settings') return ;
return ;
}
function AdminApp({ subPath }) {
return (
);
}
Object.assign(window, { AdminApp });