Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 29 additions & 17 deletions src/components/Layout.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import { useSyncExternalStore } from "react";
import { NavLink, Outlet } from "react-router-dom";
import { colors, fonts } from "../theme.js";
import { track } from "../lib/analytics.js";
import DevImportBanner from "./DevImportBanner.jsx";

const STALE_MS = 30 * 60 * 1000; // 30 minutes
const TICK_MS = 60_000;

let _now = Date.now();
let _listeners = [];
setInterval(() => { _now = Date.now(); _listeners.forEach(fn => fn()); }, TICK_MS);

function subscribeNow(cb) { _listeners.push(cb); return () => { _listeners = _listeners.filter(fn => fn !== cb); }; }
function getNow() { return _now; }

const CORE_NAV = [
{ to: "/", label: "Dashboard", icon: "◈" },
{ to: "/assets", label: "Assets", icon: "▤" },
Expand Down Expand Up @@ -47,6 +58,9 @@ export default function Layout({ sellDate, onSellDateChange, purchaseDate, onPur
: planningMode === "vehicle" ? VEHICLE_PLANNING_NAV
: null;

const now = useSyncExternalStore(subscribeNow, getNow);
const isStale = lastFetch && (now - lastFetch.getTime() > STALE_MS);

return (
<div className="gl-layout" style={{ fontFamily: fonts.mono, background: colors.bg, color: colors.text, height: "100vh", display: "flex", overflow: "hidden" }}>

Expand Down Expand Up @@ -104,7 +118,7 @@ export default function Layout({ sellDate, onSellDateChange, purchaseDate, onPur
className="gl-kofi"
onClick={() => track("kofi_click")}
aria-label="Support GreenLight on Ko-fi"
style={{ display: "block", padding: "8px 12px", marginTop: 4, opacity: 0.5, transition: "opacity 0.2s" }}>
style={{ display: "flex", alignItems: "center", padding: "8px 20px 8px 23px", marginTop: 4, opacity: 0.5, transition: "opacity 0.2s" }}>
<img src="https://storage.ko-fi.com/cdn/kofi2.png?v=6" alt="Buy Me a Coffee"
width={120} height={28} decoding="async" style={{ border: 0 }} />
</a>
Expand All @@ -116,23 +130,30 @@ export default function Layout({ sellDate, onSellDateChange, purchaseDate, onPur
{/* Top bar */}
<header className="gl-header">
<div style={{ fontSize: 13, color: colors.dim, display: "flex", alignItems: "center", gap: 8 }}>
{lastFetch && (
<>
<span className="gl-dot gl-dot-green gl-dot-pulse" />
<span style={{ color: colors.muted }}>{lastFetch.toLocaleTimeString()}</span>
</>
)}
{fetchErr && <span style={{ marginLeft: 8, color: colors.amber }}>⚠ {fetchErr}</span>}
{fetchErr && <span style={{ color: colors.amber }}>⚠ {fetchErr}</span>}
</div>
<div className="gl-header-right" style={{ display: "flex", alignItems: "center", gap: 16 }}>
<button
className="gl-btn gl-btn-ghost gl-btn-sm gl-header-refresh"
onClick={onRefresh}
disabled={fetching}
style={{ display: "flex", alignItems: "center", gap: 6 }}
>
{lastFetch && (
<span className={`gl-dot ${isStale ? "gl-dot-amber" : "gl-dot-green"} gl-dot-pulse`} />
)}
{fetching ? "↻ ..." : "↻ Refresh"}
</button>
<div className="gl-header-divider" />
<div className="gl-date-field">
<label className="gl-date-label">SELL DATE</label>
<input
type="date"
value={sellDate}
onChange={e => onSellDateChange(e.target.value)}
className="gl-date-input"
/>
</div>
{planningMode && (
<div className="gl-date-field">
<label className="gl-date-label">PURCHASE DATE</label>
Expand All @@ -144,15 +165,6 @@ export default function Layout({ sellDate, onSellDateChange, purchaseDate, onPur
/>
</div>
)}
<div className="gl-date-field">
<label className="gl-date-label">SELL DATE</label>
<input
type="date"
value={sellDate}
onChange={e => onSellDateChange(e.target.value)}
className="gl-date-input"
/>
</div>
</div>
</header>

Expand Down
21 changes: 17 additions & 4 deletions src/components/ReadinessTimeline.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,19 @@ export default function ReadinessTimeline({ readinessMonth, totalMonths, markers

const barPct = readinessMonth != null ? pct(readinessMonth) : 100;

// Sort markers by position and stagger labels above/below to avoid overlap
const sorted = [...markers].sort((a, b) => a.month - b.month);
const staggered = [];
for (let i = 0; i < sorted.length; i++) {
let above = i % 2 === 0;
if (i > 0 && Math.abs(pct(sorted[i].month) - pct(sorted[i - 1].month)) < 8) {
above = !staggered[i - 1].above;
}
staggered.push({ ...sorted[i], above });
}

return (
<div style={{ padding: "12px 0" }}>
<div style={{ padding: "30px 0 12px" }}>
{/* Track */}
<div style={{ position: "relative", height: 28, background: colors.bgInput, borderRadius: 14, overflow: "visible" }}>
{/* Filled bar */}
Expand All @@ -32,7 +43,7 @@ export default function ReadinessTimeline({ readinessMonth, totalMonths, markers
}} />

{/* Markers */}
{markers.map((m, i) => {
{staggered.map((m, i) => {
const left = pct(m.month);
const markerColor = m.type === "target" ? colors.green
: m.type === "ltDate" ? colors.blue
Expand All @@ -42,10 +53,12 @@ export default function ReadinessTimeline({ readinessMonth, totalMonths, markers
position: "absolute", top: -4, left: `${left}%`, transform: "translateX(-50%)",
width: m.type === "target" ? 3 : 2, height: 36,
background: markerColor, opacity: m.type === "target" ? 0.9 : 0.7,
borderStyle: m.type === "target" ? "dashed" : "solid",
}}>
<div style={{
position: "absolute", top: 40, left: "50%", transform: "translateX(-50%)",
position: "absolute",
...(m.above
? { bottom: 40, left: "50%", transform: "translateX(-50%)" }
: { top: 40, left: "50%", transform: "translateX(-50%)" }),
fontSize: 9, color: markerColor, fontWeight: m.type === "target" ? 600 : 400,
whiteSpace: "nowrap", letterSpacing: 0.5,
}}>
Expand Down
12 changes: 10 additions & 2 deletions src/data/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,17 @@ export const COINGECKO_TICKERS = {
pol: "polygon-ecosystem-token",
};

export const DEFAULT_PLATFORMS = {
// Known platform fees — auto-populated when importing from these platforms
export const KNOWN_PLATFORM_FEES = {
cs: { name: "ComputerShare", feePerShare: 0.10, flatFee: 10, feePercent: 0 },
gem: { name: "Gemini", feePerShare: 0, flatFee: 0, feePercent: 0.015 },
pp: { name: "Paypal", feePerShare: 0, flatFee: 0, feePercent: 0.02 },
fidelity: { name: "Fidelity", feePerShare: 0, flatFee: 0, feePercent: 0 },
};

// New users start with no platform fees — populated on import or manually in Settings
export const DEFAULT_PLATFORMS = {};

export const DEFAULT_TAX_CONFIG = {
taxMode: "progressive", // "progressive" (bracket-based) or "flat" (manual rates)
taxYear: 2025,
Expand Down Expand Up @@ -70,6 +74,9 @@ export const DEFAULT_CASH_FLOW = {
paycheckAmount: 5395.63,
paycheckFrequency: "biweekly",
firstPayDate: "2026-03-06",
spousePaycheckAmount: 0,
spousePaycheckFrequency: "biweekly",
spouseFirstPayDate: "",
expenses: [
{ id: uuid(), name: "Mortgage", amount: 1679, frequency: "monthly", dayOfMonth: 1, startDate: "2026-04-01" },
],
Expand Down Expand Up @@ -123,7 +130,7 @@ export const DEFAULT_READINESS = {
assetAppreciationRate: 0,
};

export const SCHEMA_VERSION = 3;
export const SCHEMA_VERSION = 6;

export function createDefaultState() {
return {
Expand Down Expand Up @@ -200,6 +207,7 @@ export function createSeededState() {
{ id: uuid(), accountType: "safe_harbor", platform: "Empower", balance: 15000, contributions: 0 },
],
};
state.platforms = { ...KNOWN_PLATFORM_FEES };
state.priceOverrides = { gme: 24.03, wgme: 4.30 };
return state;
}
Loading