Initial commit: Apothecary v0.4.0
This commit is contained in:
@@ -0,0 +1,405 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Apothecary — Personal Inventory</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;600&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="tokens.css" />
|
||||
<style>
|
||||
/* App chrome */
|
||||
.app-shell {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: 220px 1fr;
|
||||
}
|
||||
.sidebar {
|
||||
border-right: 1px solid var(--line);
|
||||
background: var(--bg-2);
|
||||
padding: 28px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 0 8px 28px;
|
||||
}
|
||||
.brand-mark {
|
||||
width: 32px; height: 32px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--ink);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-family: var(--serif);
|
||||
font-size: 18px;
|
||||
font-style: italic;
|
||||
color: var(--ink);
|
||||
}
|
||||
.nav-link {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 9px 12px;
|
||||
border-radius: var(--r-md);
|
||||
color: var(--ink-2);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
transition: background 100ms;
|
||||
}
|
||||
.nav-link:hover { background: var(--bg-3); color: var(--ink); }
|
||||
.nav-link.active { background: var(--surface); color: var(--ink); box-shadow: var(--shadow-sm); border: 1px solid var(--line); }
|
||||
.nav-section {
|
||||
font-size: 10px; text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--ink-3);
|
||||
padding: 16px 12px 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.inv-row:hover { background: var(--bg-2); }
|
||||
|
||||
@media (max-width: 880px) {
|
||||
.app-shell { grid-template-columns: 1fr; }
|
||||
.sidebar {
|
||||
position: fixed; bottom: 0; left: 0; right: 0; top: auto;
|
||||
height: auto;
|
||||
flex-direction: row;
|
||||
padding: 8px 12px;
|
||||
border-top: 1px solid var(--line);
|
||||
border-right: none;
|
||||
z-index: 30;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.brand, .nav-section { display: none; }
|
||||
.nav-link { white-space: nowrap; padding: 8px 12px; }
|
||||
.main { padding-bottom: 60px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
||||
|
||||
<script src="data.js?v=4"></script>
|
||||
<script type="text/babel" src="primitives.jsx?v=4"></script>
|
||||
<script type="text/babel" src="screens-1.jsx?v=4"></script>
|
||||
<script type="text/babel" src="screens-2.jsx?v=4"></script>
|
||||
<script type="text/babel" src="tweaks-panel.jsx?v=4"></script>
|
||||
<script type="text/babel">
|
||||
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
|
||||
"theme": "light",
|
||||
"dashboard": "editorial",
|
||||
"tone": "botanical",
|
||||
"accent": "sage"
|
||||
}/*EDITMODE-END*/;
|
||||
|
||||
const NAV = [
|
||||
{ key: "dashboard", label: "Dashboard", icon: "home" },
|
||||
{ key: "inventory", label: "Inventory", icon: "box" },
|
||||
{ key: "bins", label: "Bins", icon: "bin" },
|
||||
{ key: "charts", label: "Patterns", icon: "chart" },
|
||||
{ key: "settings", label: "Settings", icon: "settings" }
|
||||
];
|
||||
|
||||
const TONE_LABELS = {
|
||||
botanical: { brand: "Apothecary", tagline: "A personal log" },
|
||||
neutral: { brand: "Inventory", tagline: "Personal stockkeeping" },
|
||||
discreet: { brand: "The Cabinet", tagline: "Private register" }
|
||||
};
|
||||
|
||||
function App() {
|
||||
const [tweaks, setTweaks] = useTweaks(TWEAK_DEFAULTS);
|
||||
const [view, setView] = React.useState("dashboard");
|
||||
const [selected, setSelected] = React.useState(null);
|
||||
const [modal, setModal] = React.useState(null); // 'add' | 'consume' | 'gone' | 'audit'
|
||||
const [modalProduct, setModalProduct] = React.useState(null);
|
||||
const [tweaksOpen, setTweaksOpen] = React.useState(false);
|
||||
|
||||
const data = window.SAMPLE_DATA;
|
||||
const stats = React.useMemo(() => computeStats(data), [data]);
|
||||
|
||||
// Apply theme
|
||||
React.useEffect(() => {
|
||||
document.documentElement.setAttribute("data-theme", tweaks.theme);
|
||||
}, [tweaks.theme]);
|
||||
|
||||
// Tweaks toggle wiring
|
||||
React.useEffect(() => {
|
||||
const handler = (e) => {
|
||||
if (e.data?.type === "__activate_edit_mode") setTweaksOpen(true);
|
||||
if (e.data?.type === "__deactivate_edit_mode") setTweaksOpen(false);
|
||||
};
|
||||
window.addEventListener("message", handler);
|
||||
window.parent.postMessage({type: "__edit_mode_available"}, "*");
|
||||
return () => window.removeEventListener("message", handler);
|
||||
}, []);
|
||||
|
||||
const labels = TONE_LABELS[tweaks.tone] || TONE_LABELS.botanical;
|
||||
|
||||
const onNav = (target) => {
|
||||
if (target === "add") { setModalProduct(null); setModal("add"); }
|
||||
else if (target === "consume") { setModalProduct(null); setModal("consume"); }
|
||||
else if (target === "gone") { setModalProduct(null); setModal("gone"); }
|
||||
else if (target === "audit") { setModalProduct(null); setModal("audit"); }
|
||||
else setView(target);
|
||||
};
|
||||
const openAudit = (p) => { setModalProduct(p); setModal("audit"); };
|
||||
const openGone = (p) => { setModalProduct(p); setSelected(null); setModal("gone"); };
|
||||
const openConsume = (p) => { setModalProduct(p); setSelected(null); setModal("consume"); };
|
||||
|
||||
const handleSave = () => {
|
||||
setModal(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app-shell" data-screen-label="App">
|
||||
{/* Sidebar */}
|
||||
<aside className="sidebar">
|
||||
<div className="brand">
|
||||
<div className="brand-mark">A</div>
|
||||
<div>
|
||||
<div className="serif" style={{fontSize: 18, fontWeight: 500, lineHeight: 1}}>{labels.brand}</div>
|
||||
<div style={{fontSize: 10, color: "var(--ink-3)", textTransform: "uppercase", letterSpacing: "0.1em"}}>{labels.tagline}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="nav-section">Workspace</div>
|
||||
{NAV.map(n => (
|
||||
<button key={n.key} className={"nav-link " + (view === n.key ? "active" : "")} onClick={() => setView(n.key)}>
|
||||
<Icon name={n.icon} size={16} />
|
||||
{n.label}
|
||||
</button>
|
||||
))}
|
||||
<div className="nav-section">Quick</div>
|
||||
<button className="nav-link" onClick={() => setModal("add")}>
|
||||
<Icon name="plus" size={16} /> Add product
|
||||
</button>
|
||||
<button className="nav-link" onClick={() => setModal("consume")}>
|
||||
<Icon name="check" size={16} /> Mark finished
|
||||
</button>
|
||||
|
||||
<div style={{flex: 1}} />
|
||||
<div style={{padding: 12, fontSize: 11, color: "var(--ink-3)", borderTop: "1px solid var(--line)", marginTop: 12}}>
|
||||
<div className="mono">v0.4 · {data.products.length} items</div>
|
||||
<div style={{marginTop: 4}}>Local-only · Apr 2026</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main */}
|
||||
<main className="main parchment" style={{minWidth: 0}}>
|
||||
{view === "dashboard" && (
|
||||
<DashboardSwitch
|
||||
variant={tweaks.dashboard}
|
||||
data={data}
|
||||
stats={stats}
|
||||
onNav={onNav}
|
||||
onSelectProduct={setSelected}
|
||||
onAudit={openAudit}
|
||||
/>
|
||||
)}
|
||||
{view === "inventory" && <Inventory data={data} onSelectProduct={setSelected} onNav={onNav} />}
|
||||
{view === "bins" && <BinsView data={data} onSelectProduct={setSelected} />}
|
||||
{view === "charts" && <ChartsView data={data} stats={stats} />}
|
||||
{view === "settings" && <SettingsView data={data} tweaks={tweaks} onTweakChange={(k,v) => setTweaks({...tweaks, [k]: v})} />}
|
||||
</main>
|
||||
|
||||
{selected && (
|
||||
<ProductDetail
|
||||
product={selected}
|
||||
data={data}
|
||||
onClose={() => setSelected(null)}
|
||||
onConsume={openConsume}
|
||||
onMarkGone={openGone}
|
||||
onAudit={openAudit}
|
||||
onEdit={() => {}}
|
||||
/>
|
||||
)}
|
||||
{modal === "add" && <AddProductFlow data={data} onClose={() => setModal(null)} onSave={handleSave} />}
|
||||
{modal === "consume" && <ConsumeFlow data={data} onClose={() => setModal(null)} product={modalProduct} />}
|
||||
{modal === "gone" && <MarkGoneFlow data={data} onClose={() => setModal(null)} product={modalProduct} />}
|
||||
{modal === "audit" && <AuditFlow data={data} onClose={() => setModal(null)} product={modalProduct} />}
|
||||
|
||||
{tweaksOpen && (
|
||||
<TweaksPanel onClose={() => setTweaksOpen(false)} title="Tweaks">
|
||||
<TweakSection title="Appearance">
|
||||
<TweakRadio label="Theme" value={tweaks.theme} options={[["light","Light"],["dark","Dark"]]} onChange={v => setTweaks({...tweaks, theme: v})} />
|
||||
<TweakRadio label="Dashboard layout" value={tweaks.dashboard} options={[["editorial","Editorial"],["dense","Data-dense"],["minimal","Minimal"]]} onChange={v => setTweaks({...tweaks, dashboard: v})} />
|
||||
</TweakSection>
|
||||
<TweakSection title="Copy">
|
||||
<TweakRadio label="Tone" value={tweaks.tone} options={[["botanical","Botanical"],["neutral","Neutral"],["discreet","Discreet"]]} onChange={v => setTweaks({...tweaks, tone: v})} />
|
||||
</TweakSection>
|
||||
</TweaksPanel>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Switches between 3 dashboard variants
|
||||
const DashboardSwitch = ({variant, ...props}) => {
|
||||
if (variant === "dense") return <DashboardDense {...props} />;
|
||||
if (variant === "minimal") return <DashboardMinimal {...props} />;
|
||||
return <Dashboard {...props} />;
|
||||
};
|
||||
|
||||
// Variation 2: data-dense
|
||||
const DashboardDense = ({data, stats, onNav, onSelectProduct}) => {
|
||||
const series = stats.series90.map(s => ({date: s.date, value: s.grams}));
|
||||
return (
|
||||
<div style={{padding: "20px 28px 60px"}}>
|
||||
<div style={{display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 18, paddingBottom: 14, borderBottom: "1px solid var(--line)"}}>
|
||||
<div style={{display: "flex", alignItems: "baseline", gap: 16}}>
|
||||
<h1 className="mono" style={{fontSize: 13, margin: 0, color: "var(--ink-2)", textTransform: "uppercase", letterSpacing: "0.08em"}}>Dashboard / 2026-04-25</h1>
|
||||
<span style={{fontSize: 11, color: "var(--ink-3)"}}>· {stats.activeCount} active · {stats.consumedCount} archived</span>
|
||||
</div>
|
||||
<div style={{display: "flex", gap: 6}}>
|
||||
<Btn variant="secondary" icon="plus" onClick={() => onNav("add")}>Product</Btn>
|
||||
<Btn variant="primary" icon="check" onClick={() => onNav("consume")}>Finish</Btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{display: "grid", gridTemplateColumns: "repeat(8, 1fr)", gap: 1, background: "var(--line)", border: "1px solid var(--line)", borderRadius: "var(--r-md)", overflow: "hidden", marginBottom: 14}}>
|
||||
{[
|
||||
["DAILY g", stats.dailyAvg.toFixed(2)],
|
||||
["WEEKLY g", stats.weeklyAvg.toFixed(1)],
|
||||
["MONTHLY g", stats.monthlyAvg.toFixed(1)],
|
||||
["AVG $/g", fmt.money(stats.avgPerGram)],
|
||||
["30D SPEND", fmt.moneyShort(stats.spend30)],
|
||||
["INV VALUE", fmt.moneyShort(stats.inventoryValue)],
|
||||
["7D THC mg", stats.thcLast7],
|
||||
["DAYS SUPPLY", Math.round(stats.daysOfSupply)]
|
||||
].map(([l, v]) => (
|
||||
<div key={l} style={{padding: "12px 14px", background: "var(--surface)"}}>
|
||||
<div className="mono" style={{fontSize: 10, color: "var(--ink-3)", letterSpacing: "0.04em"}}>{l}</div>
|
||||
<div className="mono" style={{fontSize: 20, marginTop: 4, fontWeight: 500}}>{v}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14, marginBottom: 14}}>
|
||||
<Card padded={false}>
|
||||
<div style={{padding: "12px 16px", borderBottom: "1px solid var(--line)", display: "flex", justifyContent: "space-between"}}>
|
||||
<span className="mono" style={{fontSize: 11, color: "var(--ink-3)", textTransform: "uppercase", letterSpacing: "0.08em"}}>g / day · 90d</span>
|
||||
<span className="mono" style={{fontSize: 11, color: "var(--ink-2)"}}>{series.reduce((s,e)=>s+e.value,0).toFixed(1)} g total</span>
|
||||
</div>
|
||||
<div style={{padding: 14}}>
|
||||
<BarChart data={series} height={120} color="var(--sage)" />
|
||||
</div>
|
||||
</Card>
|
||||
<Card padded={false}>
|
||||
<div style={{padding: "12px 16px", borderBottom: "1px solid var(--line)"}}>
|
||||
<span className="mono" style={{fontSize: 11, color: "var(--ink-3)", textTransform: "uppercase", letterSpacing: "0.08em"}}>recent purchases</span>
|
||||
</div>
|
||||
<div>
|
||||
{[...data.products].sort((a,b)=>new Date(b.purchaseDate)-new Date(a.purchaseDate)).slice(0, 5).map(p => (
|
||||
<div key={p.id} onClick={() => onSelectProduct(p)} style={{padding: "10px 16px", borderBottom: "1px solid var(--line)", display: "flex", justifyContent: "space-between", alignItems: "center", cursor: "pointer", fontSize: 12}}>
|
||||
<div>
|
||||
<div style={{fontWeight: 500, fontSize: 13}}>{p.name}</div>
|
||||
<div style={{fontSize: 11, color: "var(--ink-3)"}}>{window.DATA_HELPERS.brandName(data, p.brandId)} · {window.DATA_HELPERS.shopName(data, p.shopId)}</div>
|
||||
</div>
|
||||
<div className="mono" style={{textAlign: "right"}}>
|
||||
<div>{fmt.money(p.price)}</div>
|
||||
<div style={{fontSize: 10, color: "var(--ink-3)"}}>{fmt.dateShort(p.purchaseDate)}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card padded={false}>
|
||||
<div style={{padding: "12px 16px", borderBottom: "1px solid var(--line)"}}>
|
||||
<span className="mono" style={{fontSize: 11, color: "var(--ink-3)", textTransform: "uppercase", letterSpacing: "0.08em"}}>active inventory · {stats.activeCount} rows</span>
|
||||
</div>
|
||||
<div style={{display: "grid", gridTemplateColumns: "2fr 1fr 0.6fr 0.6fr 0.6fr 0.8fr", padding: "8px 16px", borderBottom: "1px solid var(--line)", fontSize: 10, color: "var(--ink-3)", textTransform: "uppercase", letterSpacing: "0.08em", fontFamily: "var(--mono)"}}>
|
||||
<div>NAME / SKU</div><div>BRAND</div><div style={{textAlign: "right"}}>THC%</div><div style={{textAlign: "right"}}>$</div><div style={{textAlign: "right"}}>REM g</div><div>BIN</div>
|
||||
</div>
|
||||
{data.products.filter(p=>p.status==="active").slice(0, 8).map(p => {
|
||||
const bin = data.bins.find(b => b.id === p.binId);
|
||||
return (
|
||||
<div key={p.id} onClick={() => onSelectProduct(p)} className="inv-row" style={{display: "grid", gridTemplateColumns: "2fr 1fr 0.6fr 0.6fr 0.6fr 0.8fr", padding: "8px 16px", borderBottom: "1px solid var(--line)", fontSize: 12, cursor: "pointer", alignItems: "center"}}>
|
||||
<div>
|
||||
<div style={{fontWeight: 500}}>{p.name}</div>
|
||||
<div className="mono" style={{fontSize: 10, color: "var(--ink-3)"}}>{p.sku}</div>
|
||||
</div>
|
||||
<div style={{color: "var(--ink-2)"}}>{window.DATA_HELPERS.brandName(data, p.brandId)}</div>
|
||||
<div className="mono" style={{textAlign: "right"}}>{p.thc.toFixed(1)}</div>
|
||||
<div className="mono" style={{textAlign: "right"}}>{fmt.money(p.price)}</div>
|
||||
<div className="mono" style={{textAlign: "right"}}>{window.remainingShort(p)}</div>
|
||||
<div style={{fontSize: 11, color: "var(--ink-3)"}}>{bin?.name || "—"}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Variation 3: minimal / editorial-quiet
|
||||
const DashboardMinimal = ({data, stats, onNav, onSelectProduct}) => {
|
||||
return (
|
||||
<div style={{padding: "80px 60px", maxWidth: 900, margin: "0 auto"}}>
|
||||
<div style={{textAlign: "center", marginBottom: 80}}>
|
||||
<div className="smallcaps" style={{color: "var(--ink-3)", marginBottom: 20}}>Saturday · April 25, 2026</div>
|
||||
<div className="serif" style={{fontSize: 96, fontWeight: 400, letterSpacing: "-0.03em", lineHeight: 1, fontStyle: "italic"}}>
|
||||
{stats.dailyAvg.toFixed(2)}<span style={{fontSize: 36, fontStyle: "normal", color: "var(--ink-3)", marginLeft: 8}}>g/day</span>
|
||||
</div>
|
||||
<div style={{marginTop: 20, fontSize: 14, color: "var(--ink-2)", maxWidth: 520, margin: "20px auto 0"}}>
|
||||
{stats.activeCount} items in your cabinet, valued around {fmt.moneyShort(stats.inventoryValue)}, with roughly {Math.round(stats.daysOfSupply)} days of flower at this pace.
|
||||
</div>
|
||||
<div style={{marginTop: 8, fontSize: 13, color: "var(--ink-3)", maxWidth: 520, margin: "8px auto 0"}}>
|
||||
{stats.consumedCount} items archived · avg lifespan {Math.round(stats.avgLifespan)} days.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 0, marginBottom: 80}}>
|
||||
{[
|
||||
["Avg $ per gram", fmt.money(stats.avgPerGram)],
|
||||
["30-day spend", fmt.moneyShort(stats.spend30)],
|
||||
["7-day THC", `${stats.thcLast7} mg`]
|
||||
].map(([l,v], i) => (
|
||||
<div key={l} style={{textAlign: "center", padding: "0 20px", borderRight: i < 2 ? "1px solid var(--line)" : "none"}}>
|
||||
<div className="smallcaps" style={{color: "var(--ink-3)", marginBottom: 10}}>{l}</div>
|
||||
<div className="serif" style={{fontSize: 36, fontWeight: 500}}>{v}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{borderTop: "1px solid var(--line)", paddingTop: 40}}>
|
||||
<div style={{display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 18}}>
|
||||
<div className="smallcaps" style={{color: "var(--ink-3)"}}>Recent additions</div>
|
||||
<button onClick={() => onNav("inventory")} style={{background: "none", border: "none", fontSize: 12, color: "var(--ink-2)", cursor: "pointer", textDecoration: "underline"}}>See all →</button>
|
||||
</div>
|
||||
{[...data.products].filter(p=>p.status==="active").sort((a,b)=>new Date(b.purchaseDate)-new Date(a.purchaseDate)).slice(0, 5).map(p => (
|
||||
<div key={p.id} onClick={() => onSelectProduct(p)} style={{display: "flex", justifyContent: "space-between", alignItems: "baseline", padding: "20px 0", borderBottom: "1px solid var(--line)", cursor: "pointer"}}>
|
||||
<div>
|
||||
<div className="serif" style={{fontSize: 24, fontWeight: 500}}>{p.name}</div>
|
||||
<div style={{fontSize: 12, color: "var(--ink-3)", marginTop: 2}}>{window.DATA_HELPERS.brandName(data, p.brandId)} · {window.DATA_HELPERS.shopName(data, p.shopId)} · {fmt.dateShort(p.purchaseDate)}</div>
|
||||
</div>
|
||||
<div className="mono" style={{fontSize: 13, color: "var(--ink-2)"}}>{window.remainingShort(p)} · {fmt.money(p.price)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{marginTop: 60, display: "flex", gap: 12, justifyContent: "center"}}>
|
||||
<Btn variant="secondary" icon="plus" onClick={() => onNav("add")}>Add product</Btn>
|
||||
<Btn variant="primary" icon="check" onClick={() => onNav("consume")}>Mark finished</Btn>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user