{tabMeta[tab].title}
by FrostGamer74676 lines27.6 KB
import React, { useState, useEffect, useMemo, useCallback } from "react";
import {
Plus, X, Trash2, BookOpen, Briefcase, BarChart3,
TrendingUp, TrendingDown, Minus, ChevronRight
} from "lucide-react";
// ---------- design tokens ----------
const T = {
ink: "#0F1524",
inkDeep: "#0A0E1A",
card: "#171F35",
line: "#28324A",
parchment: "#EDE7D9",
dim: "#7C8699",
brass: "#C9A24B",
brassDim: "rgba(201,162,75,0.14)",
gain: "#5FAE7C",
gainDim: "rgba(95,174,124,0.14)",
loss: "#C1584B",
lossDim: "rgba(193,88,75,0.14)",
};
const fmtMoney = (n) => {
if (n === null || n === undefined || isNaN(n)) return "—";
const abs = Math.abs(n);
const s = abs.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
return `${n < 0 ? "-" : ""}$${s}`;
};
const fmtPct = (n) => {
if (n === null || n === undefined || isNaN(n)) return "—";
return `${(n * 100).toFixed(1)}%`;
};
const uid = () => Math.random().toString(36).slice(2, 10);
function deriveTrade(t) {
const shares = parseFloat(t.shares) || 0;
const entry = parseFloat(t.entry) || 0;
const exit = parseFloat(t.exit) || 0;
const fees = parseFloat(t.fees) || 0;
const costBasis = shares * entry;
const proceeds = shares * exit - fees;
const pl = proceeds - costBasis;
const plPct = costBasis ? pl / costBasis : 0;
let outcome = "Breakeven";
if (pl > 0) outcome = "Win";
else if (pl < 0) outcome = "Loss";
return { ...t, costBasis, proceeds, pl, plPct, outcome };
}
function deriveHolding(h) {
const shares = parseFloat(h.shares) || 0;
const avgCost = parseFloat(h.avgCost) || 0;
const price = parseFloat(h.price) || 0;
const costBasis = shares * avgCost;
const marketValue = shares * price;
const upl = marketValue - costBasis;
const uplPct = costBasis ? upl / costBasis : 0;
return { ...h, costBasis, marketValue, upl, uplPct };
}
// ---------- tiny sparkline ----------
function Sparkline({ values, width = 280, height = 64 }) {
if (!values.length) {
return (
<div style={{ height, display: "flex", alignItems: "center", justifyContent: "center", color: T.dim, fontSize: 12, fontFamily: "Inter, sans-serif" }}>
Log a trade to see your curve
</div>
);
}
const min = Math.min(0, ...values);
const max = Math.max(0, ...values);
const range = max - min || 1;
const pad = 6;
const step = values.length > 1 ? (width - pad * 2) / (values.length - 1) : 0;
const pts = values.map((v, i) => {
const x = pad + i * step;
const y = height - pad - ((v - min) / range) * (height - pad * 2);
return [x, y];
});
const zeroY = height - pad - ((0 - min) / range) * (height - pad * 2);
const path = pts.map((p, i) => `${i === 0 ? "M" : "L"}${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(" ");
const last = values[values.length - 1];
const color = last >= 0 ? T.gain : T.loss;
return (
<svg width="100%" height={height} viewBox={`0 0 ${width} ${height}`} preserveAspectRatio="none">
<line x1="0" y1={zeroY} x2={width} y2={zeroY} stroke={T.line} strokeWidth="1" strokeDasharray="2,3" />
<path d={path} fill="none" stroke={color} strokeWidth="1.75" />
{pts.length > 0 && (
<circle cx={pts[pts.length - 1][0]} cy={pts[pts.length - 1][1]} r="3" fill={color} />
)}
</svg>
);
}
// ---------- reusable bits ----------
function Field({ label, children }) {
return (
<label style={{ display: "block", marginBottom: 14 }}>
<span style={{ display: "block", fontSize: 11, letterSpacing: "0.08em", textTransform: "uppercase", color: T.dim, marginBottom: 6, fontFamily: "Inter, sans-serif" }}>
{label}
</span>
{children}
</label>
);
}
const inputStyle = {
width: "100%",
background: T.ink,
border: `1px solid ${T.line}`,
borderRadius: 8,
padding: "10px 12px",
color: T.parchment,
fontSize: 15,
fontFamily: "IBM Plex Mono, monospace",
outline: "none",
boxSizing: "border-box",
};
function Segmented({ options, value, onChange }) {
return (
<div style={{ display: "flex", background: T.ink, border: `1px solid ${T.line}`, borderRadius: 8, padding: 3 }}>
{options.map((opt) => (
<button
key={opt}
type="button"
onClick={() => onChange(opt)}
style={{
flex: 1,
padding: "8px 0",
borderRadius: 6,
border: "none",
fontFamily: "Inter, sans-serif",
fontSize: 13,
fontWeight: 600,
cursor: "pointer",
background: value === opt ? T.brass : "transparent",
color: value === opt ? T.inkDeep : T.dim,
transition: "background 0.15s ease",
}}
>
{opt}
</button>
))}
</div>
);
}
function OutcomeBadge({ pl }) {
const positive = pl > 0;
const flat = pl === 0;
const color = flat ? T.dim : positive ? T.gain : T.loss;
const bg = flat ? "transparent" : positive ? T.gainDim : T.lossDim;
const Icon = flat ? Minus : positive ? TrendingUp : TrendingDown;
return (
<span style={{
display: "inline-flex", alignItems: "center", gap: 4,
color, background: bg, padding: "3px 8px", borderRadius: 999,
fontSize: 12, fontFamily: "IBM Plex Mono, monospace", fontWeight: 600,
}}>
<Icon size={12} strokeWidth={2.5} />
{fmtMoney(pl)}
</span>
);
}
// ---------- main app ----------
export default function TradeLedger() {
const [tab, setTab] = useState("log");
const [trades, setTrades] = useState([]);
const [holdings, setHoldings] = useState([]);
const [loaded, setLoaded] = useState(false);
const [sheet, setSheet] = useState(null); // { type: 'trade'|'holding', data }
useEffect(() => {
(async () => {
try {
const t = await window.storage.get("ledger:trades", false);
if (t?.value) setTrades(JSON.parse(t.value));
} catch (e) { /* no data yet */ }
try {
const h = await window.storage.get("ledger:holdings", false);
if (h?.value) setHoldings(JSON.parse(h.value));
} catch (e) { /* no data yet */ }
setLoaded(true);
})();
}, []);
const saveTrades = useCallback(async (next) => {
setTrades(next);
try { await window.storage.set("ledger:trades", JSON.stringify(next), false); }
catch (e) { console.error("save trades failed", e); }
}, []);
const saveHoldings = useCallback(async (next) => {
setHoldings(next);
try { await window.storage.set("ledger:holdings", JSON.stringify(next), false); }
catch (e) { console.error("save holdings failed", e); }
}, []);
const derivedTrades = useMemo(() =>
trades.map(deriveTrade).sort((a, b) => (b.date || "").localeCompare(a.date || "")),
[trades]
);
const derivedHoldings = useMemo(() => holdings.map(deriveHolding), [holdings]);
const stats = useMemo(() => {
const closed = derivedTrades;
const wins = closed.filter((t) => t.outcome === "Win").length;
const losses = closed.filter((t) => t.outcome === "Loss").length;
const total = closed.length;
const winRate = total ? wins / total : 0;
const totalPL = closed.reduce((s, t) => s + t.pl, 0);
const avgPL = total ? totalPL / total : 0;
const best = total ? Math.max(...closed.map((t) => t.pl)) : 0;
const worst = total ? Math.min(...closed.map((t) => t.pl)) : 0;
const holdingsValue = derivedHoldings.reduce((s, h) => s + h.marketValue, 0);
const holdingsUPL = derivedHoldings.reduce((s, h) => s + h.upl, 0);
// cumulative curve in chronological order
const chrono = [...closed].sort((a, b) => (a.date || "").localeCompare(b.date || ""));
let running = 0;
const curve = chrono.map((t) => (running += t.pl));
return { wins, losses, total, winRate, totalPL, avgPL, best, worst, holdingsValue, holdingsUPL, curve };
}, [derivedTrades, derivedHoldings]);
const openNewTrade = () => setSheet({
type: "trade",
data: { id: null, date: new Date().toISOString().slice(0, 10), ticker: "", side: "Sell", shares: "", entry: "", exit: "", fees: "0", notes: "" },
});
const openEditTrade = (t) => setSheet({ type: "trade", data: t });
const openNewHolding = () => setSheet({
type: "holding",
data: { id: null, ticker: "", shares: "", avgCost: "", price: "", notes: "" },
});
const openEditHolding = (h) => setSheet({ type: "holding", data: h });
const handleSaveTrade = (data) => {
if (!data.ticker || !data.shares || !data.entry) return;
if (data.id) {
saveTrades(trades.map((t) => (t.id === data.id ? data : t)));
} else {
saveTrades([...trades, { ...data, id: uid() }]);
}
setSheet(null);
};
const handleDeleteTrade = (id) => {
saveTrades(trades.filter((t) => t.id !== id));
setSheet(null);
};
const handleSaveHolding = (data) => {
if (!data.ticker || !data.shares) return;
if (data.id) {
saveHoldings(holdings.map((h) => (h.id === data.id ? data : h)));
} else {
saveHoldings([...holdings, { ...data, id: uid() }]);
}
setSheet(null);
};
const handleDeleteHolding = (id) => {
saveHoldings(holdings.filter((h) => h.id !== id));
setSheet(null);
};
const tabMeta = {
log: { title: "Trade Log", sub: "Closed positions & outcomes" },
holdings: { title: "Holdings", sub: "What you currently own" },
summary: { title: "Summary", sub: "Your track record" },
};
return (
<div style={{
fontFamily: "Inter, sans-serif",
background: T.ink,
color: T.parchment,
minHeight: "100vh",
maxWidth: 480,
margin: "0 auto",
position: "relative",
display: "flex",
flexDirection: "column",
}}>
<style>{`
@import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600;9..144,700&family=IBM+Plex+Mono:wght@400;500;600&family=Inter:wght@400;500;600;700&display=swap');
* { box-sizing: border-box; }
input::placeholder, textarea::placeholder { color: ${T.dim}; opacity: 0.7; }
input:focus, textarea:focus, select:focus { border-color: ${T.brass} !important; }
button { font-family: inherit; }
::-webkit-scrollbar { width: 0; height: 0; }
`}</style>
{/* header */}
<div style={{
position: "sticky", top: 0, zIndex: 10,
background: T.inkDeep, borderBottom: `1px solid ${T.line}`,
padding: "18px 20px 14px",
}}>
<div style={{ display: "flex", alignItems: "baseline", gap: 8 }}>
<span style={{ color: T.brass, fontFamily: "Fraunces, serif", fontSize: 13, fontWeight: 600 }}>§</span>
<h1 style={{ margin: 0, fontFamily: "Fraunces, serif", fontWeight: 600, fontSize: 22, letterSpacing: "-0.01em" }}>
{tabMeta[tab].title}
</h1>
</div>
<p style={{ margin: "2px 0 0 20px", fontSize: 12.5, color: T.dim }}>{tabMeta[tab].sub}</p>
</div>
{/* content */}
<div style={{ flex: 1, overflowY: "auto", padding: "16px 16px 100px" }}>
{!loaded ? (
<div style={{ textAlign: "center", color: T.dim, padding: "40px 0", fontSize: 13 }}>Loading your ledger…</div>
) : tab === "log" ? (
<LogList trades={derivedTrades} onEdit={openEditTrade} />
) : tab === "holdings" ? (
<HoldingsList holdings={derivedHoldings} onEdit={openEditHolding} />
) : (
<SummaryView stats={stats} />
)}
</div>
{/* FAB */}
{tab !== "summary" && (
<button
onClick={tab === "log" ? openNewTrade : openNewHolding}
style={{
position: "absolute", right: 20, bottom: 88, zIndex: 20,
width: 52, height: 52, borderRadius: "50%",
background: T.brass, border: "none", color: T.inkDeep,
display: "flex", alignItems: "center", justifyContent: "center",
boxShadow: "0 6px 16px rgba(0,0,0,0.4)", cursor: "pointer",
}}
aria-label={tab === "log" ? "Add trade" : "Add holding"}
>
<Plus size={24} strokeWidth={2.5} />
</button>
)}
{/* bottom nav */}
<div style={{
position: "sticky", bottom: 0, background: T.inkDeep,
borderTop: `1px solid ${T.line}`, display: "flex", padding: "8px 8px calc(env(safe-area-inset-bottom, 0px) + 8px)",
}}>
{[
{ id: "log", label: "Log", icon: BookOpen },
{ id: "holdings", label: "Holdings", icon: Briefcase },
{ id: "summary", label: "Summary", icon: BarChart3 },
].map(({ id, label, icon: Icon }) => (
<button
key={id}
onClick={() => setTab(id)}
style={{
flex: 1, background: "none", border: "none", cursor: "pointer",
display: "flex", flexDirection: "column", alignItems: "center", gap: 3,
padding: "8px 0", color: tab === id ? T.brass : T.dim,
}}
>
<Icon size={20} strokeWidth={tab === id ? 2.4 : 2} />
<span style={{ fontSize: 10.5, fontWeight: 600, letterSpacing: "0.03em" }}>{label}</span>
</button>
))}
</div>
{/* sheet */}
{sheet?.type === "trade" && (
<TradeSheet data={sheet.data} onClose={() => setSheet(null)} onSave={handleSaveTrade} onDelete={handleDeleteTrade} />
)}
{sheet?.type === "holding" && (
<HoldingSheet data={sheet.data} onClose={() => setSheet(null)} onSave={handleSaveHolding} onDelete={handleDeleteHolding} />
)}
</div>
);
}
// ---------- log list ----------
function LogList({ trades, onEdit }) {
if (!trades.length) {
return <EmptyState label="No trades logged yet" hint="Tap + to record your first closed trade." />;
}
// group by month
const groups = {};
trades.forEach((t) => {
const key = (t.date || "Undated").slice(0, 7);
groups[key] = groups[key] || [];
groups[key].push(t);
});
return (
<div>
{Object.entries(groups).map(([month, items]) => (
<div key={month} style={{ marginBottom: 18 }}>
<div style={{ fontSize: 11, letterSpacing: "0.08em", textTransform: "uppercase", color: T.dim, marginBottom: 8, paddingLeft: 4 }}>
{monthLabel(month)}
</div>
<div style={{ background: T.card, borderRadius: 12, border: `1px solid ${T.line}`, overflow: "hidden" }}>
{items.map((t, i) => (
<button
key={t.id}
onClick={() => onEdit(t)}
style={{
width: "100%", textAlign: "left", background: "none", border: "none", cursor: "pointer",
padding: "12px 14px", display: "flex", alignItems: "center", justifyContent: "space-between",
borderTop: i === 0 ? "none" : `1px solid ${T.line}`, color: T.parchment,
}}
>
<div style={{ minWidth: 0 }}>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<span style={{ fontFamily: "IBM Plex Mono, monospace", fontWeight: 600, fontSize: 14.5 }}>{t.ticker}</span>
<span style={{ fontSize: 11, color: T.dim }}>{t.side}</span>
</div>
<div style={{ fontSize: 12, color: T.dim, marginTop: 2 }}>
{t.shares} sh · {fmtMoney(parseFloat(t.entry))} → {fmtMoney(parseFloat(t.exit))}
</div>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 6, flexShrink: 0 }}>
<OutcomeBadge pl={t.pl} />
<ChevronRight size={14} color={T.dim} />
</div>
</button>
))}
</div>
</div>
))}
</div>
);
}
function monthLabel(key) {
if (key === "Undated") return "Undated";
const [y, m] = key.split("-");
const d = new Date(parseInt(y), parseInt(m) - 1, 1);
return d.toLocaleDateString("en-US", { month: "long", year: "numeric" });
}
// ---------- holdings list ----------
function HoldingsList({ holdings, onEdit }) {
if (!holdings.length) {
return <EmptyState label="No holdings tracked yet" hint="Tap + to add a position you currently own." />;
}
const totalValue = holdings.reduce((s, h) => s + h.marketValue, 0);
const totalUPL = holdings.reduce((s, h) => s + h.upl, 0);
return (
<div>
<div style={{
background: T.card, border: `1px solid ${T.line}`, borderRadius: 12,
padding: "16px 16px", marginBottom: 16, display: "flex", justifyContent: "space-between",
}}>
<div>
<div style={{ fontSize: 11, color: T.dim, textTransform: "uppercase", letterSpacing: "0.06em" }}>Market Value</div>
<div style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: 20, fontWeight: 600, marginTop: 4 }}>{fmtMoney(totalValue)}</div>
</div>
<div style={{ textAlign: "right" }}>
<div style={{ fontSize: 11, color: T.dim, textTransform: "uppercase", letterSpacing: "0.06em" }}>Unrealized</div>
<div style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: 20, fontWeight: 600, marginTop: 4, color: totalUPL >= 0 ? T.gain : T.loss }}>
{fmtMoney(totalUPL)}
</div>
</div>
</div>
<div style={{ background: T.card, borderRadius: 12, border: `1px solid ${T.line}`, overflow: "hidden" }}>
{holdings.map((h, i) => (
<button
key={h.id}
onClick={() => onEdit(h)}
style={{
width: "100%", textAlign: "left", background: "none", border: "none", cursor: "pointer",
padding: "12px 14px", display: "flex", alignItems: "center", justifyContent: "space-between",
borderTop: i === 0 ? "none" : `1px solid ${T.line}`, color: T.parchment,
}}
>
<div>
<div style={{ fontFamily: "IBM Plex Mono, monospace", fontWeight: 600, fontSize: 14.5 }}>{h.ticker}</div>
<div style={{ fontSize: 12, color: T.dim, marginTop: 2 }}>{h.shares} sh @ {fmtMoney(parseFloat(h.avgCost))} avg</div>
</div>
<div style={{ textAlign: "right" }}>
<div style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: 14 }}>{fmtMoney(h.marketValue)}</div>
<div style={{ fontSize: 12, color: h.upl >= 0 ? T.gain : T.loss, marginTop: 2 }}>
{h.upl >= 0 ? "+" : ""}{fmtMoney(h.upl)} ({fmtPct(h.uplPct)})
</div>
</div>
</button>
))}
</div>
</div>
);
}
// ---------- summary ----------
function SummaryView({ stats }) {
const statCards = [
{ label: "Total Realized P/L", value: fmtMoney(stats.totalPL), color: stats.totalPL >= 0 ? T.gain : T.loss },
{ label: "Win Rate", value: fmtPct(stats.winRate), color: T.parchment },
{ label: "Wins / Losses", value: `${stats.wins} / ${stats.losses}`, color: T.parchment },
{ label: "Avg P/L per Trade", value: fmtMoney(stats.avgPL), color: stats.avgPL >= 0 ? T.gain : T.loss },
{ label: "Best Trade", value: fmtMoney(stats.best), color: T.gain },
{ label: "Worst Trade", value: fmtMoney(stats.worst), color: T.loss },
];
return (
<div>
<div style={{ background: T.card, border: `1px solid ${T.line}`, borderRadius: 12, padding: "16px 14px", marginBottom: 16 }}>
<div style={{ fontSize: 11, color: T.dim, textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 8 }}>
Cumulative Realized P/L
</div>
<Sparkline values={stats.curve} />
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10, marginBottom: 16 }}>
{statCards.map((s) => (
<div key={s.label} style={{ background: T.card, border: `1px solid ${T.line}`, borderRadius: 12, padding: "12px 14px" }}>
<div style={{ fontSize: 10.5, color: T.dim, textTransform: "uppercase", letterSpacing: "0.05em" }}>{s.label}</div>
<div style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: 17, fontWeight: 600, marginTop: 5, color: s.color }}>{s.value}</div>
</div>
))}
</div>
<div style={{ background: T.card, border: `1px solid ${T.line}`, borderRadius: 12, padding: "14px", display: "flex", justifyContent: "space-between" }}>
<div>
<div style={{ fontSize: 11, color: T.dim, textTransform: "uppercase", letterSpacing: "0.06em" }}>Holdings Value</div>
<div style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: 16, fontWeight: 600, marginTop: 4 }}>{fmtMoney(stats.holdingsValue)}</div>
</div>
<div style={{ textAlign: "right" }}>
<div style={{ fontSize: 11, color: T.dim, textTransform: "uppercase", letterSpacing: "0.06em" }}>Unrealized</div>
<div style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: 16, fontWeight: 600, marginTop: 4, color: stats.holdingsUPL >= 0 ? T.gain : T.loss }}>
{fmtMoney(stats.holdingsUPL)}
</div>
</div>
</div>
</div>
);
}
function EmptyState({ label, hint }) {
return (
<div style={{ textAlign: "center", padding: "60px 20px", color: T.dim }}>
<div style={{ fontFamily: "Fraunces, serif", fontSize: 17, color: T.parchment, marginBottom: 6 }}>{label}</div>
<div style={{ fontSize: 13 }}>{hint}</div>
</div>
);
}
// ---------- sheets (bottom modal forms) ----------
function SheetShell({ title, onClose, onDelete, children }) {
return (
<div style={{ position: "fixed", inset: 0, zIndex: 50, display: "flex", alignItems: "flex-end", maxWidth: 480, margin: "0 auto" }}>
<div onClick={onClose} style={{ position: "absolute", inset: 0, background: "rgba(0,0,0,0.55)" }} />
<div style={{
position: "relative", width: "100%", background: T.inkDeep, borderTop: `1px solid ${T.line}`,
borderTopLeftRadius: 18, borderTopRightRadius: 18, padding: "18px 20px calc(env(safe-area-inset-bottom, 0px) + 20px)",
maxHeight: "88vh", overflowY: "auto",
}}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
<h2 style={{ margin: 0, fontFamily: "Fraunces, serif", fontSize: 19, fontWeight: 600 }}>{title}</h2>
<div style={{ display: "flex", gap: 8 }}>
{onDelete && (
<button onClick={onDelete} style={{ background: T.lossDim, border: "none", borderRadius: 8, padding: 8, color: T.loss, cursor: "pointer" }}>
<Trash2 size={16} />
</button>
)}
<button onClick={onClose} style={{ background: T.line, border: "none", borderRadius: 8, padding: 8, color: T.parchment, cursor: "pointer" }}>
<X size={16} />
</button>
</div>
</div>
{children}
</div>
</div>
);
}
function TradeSheet({ data, onClose, onSave, onDelete }) {
const [form, setForm] = useState(data);
const set = (k) => (e) => setForm({ ...form, [k]: e.target.value });
const preview = deriveTrade(form);
const validCost = preview.costBasis > 0;
return (
<SheetShell title={form.id ? "Edit Trade" : "New Trade"} onClose={onClose} onDelete={form.id ? () => onDelete(form.id) : null}>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
<Field label="Ticker">
<input style={inputStyle} value={form.ticker} onChange={(e) => setForm({ ...form, ticker: e.target.value.toUpperCase() })} placeholder="AAPL" />
</Field>
<Field label="Date">
<input type="date" style={inputStyle} value={form.date} onChange={set("date")} />
</Field>
</div>
<Field label="Side">
<Segmented options={["Buy", "Sell"]} value={form.side} onChange={(v) => setForm({ ...form, side: v })} />
</Field>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 12 }}>
<Field label="Shares">
<input type="number" inputMode="decimal" style={inputStyle} value={form.shares} onChange={set("shares")} placeholder="0" />
</Field>
<Field label="Entry $">
<input type="number" inputMode="decimal" style={inputStyle} value={form.entry} onChange={set("entry")} placeholder="0.00" />
</Field>
<Field label="Exit $">
<input type="number" inputMode="decimal" style={inputStyle} value={form.exit} onChange={set("exit")} placeholder="0.00" />
</Field>
</div>
<Field label="Fees $">
<input type="number" inputMode="decimal" style={inputStyle} value={form.fees} onChange={set("fees")} placeholder="0.00" />
</Field>
<Field label="Notes">
<textarea style={{ ...inputStyle, fontFamily: "Inter, sans-serif", minHeight: 60, resize: "vertical" }} value={form.notes} onChange={set("notes")} placeholder="Why'd you take this trade?" />
</Field>
{validCost && (
<div style={{ background: T.card, border: `1px solid ${T.line}`, borderRadius: 10, padding: "12px 14px", marginBottom: 16, display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<span style={{ fontSize: 12, color: T.dim }}>Result</span>
<OutcomeBadge pl={preview.pl} />
</div>
)}
<button
onClick={() => onSave(form)}
disabled={!form.ticker || !form.shares || !form.entry}
style={{
width: "100%", padding: "13px 0", borderRadius: 10, border: "none",
background: (!form.ticker || !form.shares || !form.entry) ? T.line : T.brass,
color: (!form.ticker || !form.shares || !form.entry) ? T.dim : T.inkDeep,
fontWeight: 700, fontSize: 15, cursor: "pointer",
}}
>
Save Trade
</button>
</SheetShell>
);
}
function HoldingSheet({ data, onClose, onSave, onDelete }) {
const [form, setForm] = useState(data);
const set = (k) => (e) => setForm({ ...form, [k]: e.target.value });
const preview = deriveHolding(form);
const valid = form.ticker && form.shares;
return (
<SheetShell title={form.id ? "Edit Holding" : "New Holding"} onClose={onClose} onDelete={form.id ? () => onDelete(form.id) : null}>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
<Field label="Ticker">
<input style={inputStyle} value={form.ticker} onChange={(e) => setForm({ ...form, ticker: e.target.value.toUpperCase() })} placeholder="MSFT" />
</Field>
<Field label="Shares">
<input type="number" inputMode="decimal" style={inputStyle} value={form.shares} onChange={set("shares")} placeholder="0" />
</Field>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
<Field label="Avg Cost $">
<input type="number" inputMode="decimal" style={inputStyle} value={form.avgCost} onChange={set("avgCost")} placeholder="0.00" />
</Field>
<Field label="Current Price $">
<input type="number" inputMode="decimal" style={inputStyle} value={form.price} onChange={set("price")} placeholder="0.00" />
</Field>
</div>
<Field label="Notes">
<textarea style={{ ...inputStyle, fontFamily: "Inter, sans-serif", minHeight: 60, resize: "vertical" }} value={form.notes} onChange={set("notes")} placeholder="Thesis, target, stop…" />
</Field>
{form.shares && form.avgCost && form.price && (
<div style={{ background: T.card, border: `1px solid ${T.line}`, borderRadius: 10, padding: "12px 14px", marginBottom: 16, display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<span style={{ fontSize: 12, color: T.dim }}>Unrealized P/L</span>
<OutcomeBadge pl={preview.upl} />
</div>
)}
<button
onClick={() => onSave(form)}
disabled={!valid}
style={{
width: "100%", padding: "13px 0", borderRadius: 10, border: "none",
background: !valid ? T.line : T.brass,
color: !valid ? T.dim : T.inkDeep,
fontWeight: 700, fontSize: 15, cursor: "pointer",
}}
>
Save Holding
</button>
</SheetShell>
);
}Game Source: {tabMeta[tab].title}
Creator: FrostGamer74
Libraries: none
Complexity: complex (676 lines, 27.6 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: tabmeta-tab-title-frostgamer74" to link back to the original. Then publish at arcadelab.ai/publish.