/* global React */
const { useState: useStateS, useEffect: useEffectS, useMemo: useMemoS, useRef: useRefS } = React;
// =============================================================================
// PHOTO SLOTS — slot_key → { label, recommended size, page }
// =============================================================================
window.PHOTO_SLOTS = [
{ key: 'hero-photo', label: 'Homepage Hero', page: 'Homepage', size: '800 × 1000 px' },
{ key: 'press-on-set-01', label: 'Press-On Teaser 1', page: 'Homepage', size: '480 × 560 px' },
{ key: 'press-on-set-02', label: 'Press-On Teaser 2', page: 'Homepage', size: '480 × 560 px' },
{ key: 'gallery-1', label: 'Gallery 1 — Aurora Chrome', page: 'Gallery', size: '400 × 533 px' },
{ key: 'gallery-2', label: 'Gallery 2 — Hand-painted', page: 'Gallery', size: '400 × 533 px' },
{ key: 'gallery-3', label: 'Gallery 3 — Micro French', page: 'Gallery', size: '400 × 533 px' },
{ key: 'gallery-4', label: 'Gallery 4 — Pearl Drop', page: 'Gallery', size: '400 × 533 px' },
{ key: 'gallery-5', label: 'Gallery 5 — Cat Eye', page: 'Gallery', size: '400 × 533 px' },
{ key: 'gallery-6', label: 'Gallery 6 — Butter Nude', page: 'Gallery', size: '400 × 533 px' },
{ key: 'gallery-7', label: 'Gallery 7 — Cherry Blossom', page: 'Gallery', size: '400 × 533 px' },
{ key: 'gallery-8', label: 'Gallery 8 — Mirror Chrome', page: 'Gallery', size: '400 × 533 px' },
{ key: 'gallery-9', label: 'Gallery 9 — Lavender French', page: 'Gallery', size: '400 × 533 px' },
{ key: 'gallery-10', label: 'Gallery 10 — Bow Charms', page: 'Gallery', size: '400 × 533 px' },
];
// =============================================================================
// PHOTOS
// =============================================================================
window.PhotosSection = function PhotosSection() {
const [photos, setPhotos] = useStateS({});
const [loading, setLoading] = useStateS(true);
const [uploadingKey, setUploadingKey] = useStateS(null);
const [err, setErr] = useStateS('');
function load() {
setLoading(true);
window.LQ.request('/photos').then((p) => { setPhotos(p || {}); setLoading(false); });
}
useEffectS(load, []);
async function upload(slotKey, file, label) {
setUploadingKey(slotKey); setErr('');
try {
await window.LQ.uploadPhoto(slotKey, file, label);
load();
} catch (e) {
setErr(e.message);
} finally {
setUploadingKey(null);
}
}
async function remove(slotKey) {
if (!confirm('Remove this photo?')) return;
await window.LQ.request(`/photos/${slotKey}`, { method: 'DELETE' });
load();
}
const grouped = useMemoS(() => {
const g = {};
window.PHOTO_SLOTS.forEach((s) => { (g[s.page] = g[s.page] || []).push(s); });
return g;
}, []);
if (loading) return
Loading…
;
return (
<>
Upload real photos to replace placeholders on the public site. Use JPG, PNG, or WebP — max 8 MB. Aim for the recommended size for crisp display.
{err && {err}
}
{Object.entries(grouped).map(([page, slots]) => (
{page}
{slots.filter((s) => photos[s.key]).length} / {slots.length} uploaded
{slots.map((s) => {
const p = photos[s.key];
return (
{s.label}
{s.size}
{p && }
);
})}
))}
>
);
};
// =============================================================================
// SHOP (Press-on products)
// =============================================================================
const CATEGORIES = ['Chrome', 'Florals', 'French', 'Glitter', '3D Charms', 'Minimal'];
const SHAPES = ['Almond', 'Coffin', 'Stiletto', 'Square', 'Short Square', 'Oval', 'Short Oval', 'Ballerina'];
const TAG_OPTIONS = ['Bestseller', 'New', 'Bridal', 'Hand-painted'];
window.ShopSection = function ShopSection() {
const [list, setList] = useStateS([]);
const [loading, setLoading] = useStateS(true);
const [editing, setEditing] = useStateS(null);
const [adding, setAdding] = useStateS(false);
function load() {
setLoading(true);
window.LQ.request('/products').then((r) => { setList(r || []); setLoading(false); });
}
useEffectS(load, []);
async function del(id) {
if (!confirm('Deactivate this product?')) return;
await window.LQ.request(`/products/${id}`, { method: 'DELETE' });
load();
}
if (loading) return Loading…
;
return (
<>
{list.length} products active.
| Photo | Name | Category | Shape | Price | Stock | Tags | Actions |
{list.map((p) => (
|
|
{p.name} |
{p.category} |
{p.shape} |
{window.LQ_fmtINR(p.price)} |
{p.stock} |
{(p.tags || []).map((t) => {t})} |
|
))}
{(editing || adding) && (
{ setEditing(null); setAdding(false); }}
onSaved={() => { setEditing(null); setAdding(false); load(); }}
/>
)}
>
);
};
window.ProductModal = function ProductModal({ product, onClose, onSaved }) {
const isEdit = !!product;
const [form, setForm] = useStateS({
name: product?.name || '',
category: product?.category || CATEGORIES[0],
shape: product?.shape || SHAPES[0],
price: product?.price || '',
purchase_price: product?.purchase_price || '',
stock: product?.stock || 0,
low_stock_alert: product?.low_stock_alert || 3,
tags: product?.tags || [],
photo_path: product?.photo_path || '',
is_active: product?.is_active ?? true,
});
const [saving, setSaving] = useStateS(false);
const [uploading, setUploading] = useStateS(false);
const [err, setErr] = useStateS('');
function set(k, v) { setForm((f) => ({ ...f, [k]: v })); }
function toggleTag(t) {
setForm((f) => ({ ...f, tags: f.tags.includes(t) ? f.tags.filter((x) => x !== t) : [...f.tags, t] }));
}
async function uploadPhoto(file) {
setUploading(true); setErr('');
try {
const slotKey = `shop-product-${product?.id || Date.now()}`;
const r = await window.LQ.uploadPhoto(slotKey, file, form.name || 'Product');
set('photo_path', r.file_path);
} catch (e) {
setErr(e.message);
} finally {
setUploading(false);
}
}
async function save(e) {
e.preventDefault();
setSaving(true); setErr('');
try {
const body = { ...form };
body.price = Number(body.price);
body.purchase_price = body.purchase_price === '' ? null : Number(body.purchase_price);
body.stock = Number(body.stock);
body.low_stock_alert = Number(body.low_stock_alert);
if (isEdit) await window.LQ.request(`/products/${product.id}`, { method: 'PATCH', body });
else await window.LQ.request('/products', { method: 'POST', body });
onSaved();
} catch (e) {
setErr(e.message);
} finally {
setSaving(false);
}
}
const margin = (form.price && form.purchase_price) ?
Math.round(((form.price - form.purchase_price) / form.price) * 100) : null;
return (
e.stopPropagation()}>
{isEdit ? 'Edit Product' : 'Add Product'}
);
};
// =============================================================================
// INVENTORY (compact view, inline editing)
// =============================================================================
window.InventorySection = function InventorySection() {
const [list, setList] = useStateS([]);
const [loading, setLoading] = useStateS(true);
function load() {
setLoading(true);
window.LQ.request('/products').then((r) => { setList(r || []); setLoading(false); });
}
useEffectS(load, []);
async function update(id, patch) {
await window.LQ.request(`/products/${id}`, { method: 'PATCH', body: patch });
load();
}
if (loading) return Loading…
;
return (
<>
Adjust stock and pricing inline. Stock auto-decrements when an order ships.
>
);
};
// =============================================================================
// SHIPPING (orders + provider toggle)
// =============================================================================
window.ShippingSection = function ShippingSection() {
const [orders, setOrders] = useStateS([]);
const [shipping, setShipping] = useStateS(null);
const [loading, setLoading] = useStateS(true);
function load() {
setLoading(true);
Promise.all([
window.LQ.request('/orders'),
window.LQ.request('/settings/shipping').catch(() => ({ provider: 'manual' })),
]).then(([o, s]) => {
setOrders(o || []);
setShipping(s || { provider: 'manual' });
setLoading(false);
});
}
useEffectS(load, []);
async function setStatus(id, status) {
await window.LQ.request(`/orders/${id}`, { method: 'PATCH', body: { status } });
load();
}
async function printLabel(o) {
if (shipping.provider === 'manual') {
window.LQ_printManualLabel(o, shipping);
await window.LQ.request(`/orders/${o.id}`, { method: 'PATCH', body: { label_printed: true } });
load();
return;
}
// Shiprocket flow — placeholder until live integration
alert(`Shiprocket label for ${o.order_code} will be fetched from API once credentials are saved + tested.`);
await window.LQ.request(`/orders/${o.id}`, { method: 'PATCH', body: { label_printed: true } });
load();
}
async function saveShipping() {
await window.LQ.request('/settings/shipping', { method: 'PUT', body: shipping });
alert('Shipping settings saved.');
}
function set(k, v) { setShipping({ ...shipping, [k]: v }); }
if (loading) return Loading…
;
return (
<>
Orders to Fulfil
{orders.filter((o) => ['pending','processing'].includes(o.status)).length} pending
{orders.length === 0 ?
No orders yet.
:
| Code | Customer | Product | Total | Status | Label | Actions |
{orders.map((o) => (
| {o.order_code} |
{o.customer_name} {o.customer_mobile} |
{o.product_name}{o.quantity > 1 && ` ×${o.quantity}`} |
{window.LQ_fmtINR(o.total)} |
{o.status} |
{o.label_printed ? Printed : } |
{o.status === 'pending' && }
{o.status === 'processing' && }
{o.status === 'shipped' && }
{!['cancelled','delivered'].includes(o.status) && }
|
))}
}
>
);
};