🎮ArcadeLab

{tabMeta[tab].title}

by FrostGamer74
676 lines27.6 KB
▶ Play


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.