From ffdc0fd9e8c362565449eedf5e0a1275e702bfdd Mon Sep 17 00:00:00 2001 From: asepharyana Date: Wed, 16 Sep 2026 15:33:18 +0700 Subject: [PATCH] =?UTF-8?q?feat(ui):=20polish=20pass=20=E2=80=94=20theme-a?= =?UTF-8?q?ware=20charts,=20brand=20nav,=20search=20&=20date=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dashboard foreign-flow chart line now uses the CSS --primary token (hsl) so it matches light/dark instead of a hardcoded blue; Portfolio bar chart gets themed bars with rounded corners + alpha steps - Nav: logo mark (📈 in primary tile), active item left accent bar, active-state matching for sub-routes (/report/:ticker now highlights Report... actually Dashboard/Screener etc) - TickerSearch: cleaner searchbox with magnifier icon + 'Buka' button, aria-label - Login page: centered branded header (logo + FlowSight), vertically centered card - Sorotan cards + Screener show company name under ticker when available - Routines run history + Alerts event/alert lists: formatted dates (16 Sep 07:30), status badges (ok/error), credits-used display - Dashboard hero/misc spacing polish --- web/src/index.tsx | 46 ++++++++++++++++++++++++++----------- web/src/pages/Alerts.tsx | 13 +++++++++-- web/src/pages/Dashboard.tsx | 24 ++++++++++++++----- web/src/pages/Portfolio.tsx | 15 +++++++++++- web/src/pages/Routines.tsx | 19 ++++++++++++++- 5 files changed, 93 insertions(+), 24 deletions(-) diff --git a/web/src/index.tsx b/web/src/index.tsx index 72053d8..03c32c4 100644 --- a/web/src/index.tsx +++ b/web/src/index.tsx @@ -27,17 +27,24 @@ function Nav(props: { loggedIn: boolean }) { const loc = useLocation(); // Belum login: hanya Dashboard yg terlihat (fitur lain di-hide). const items = () => props.loggedIn ? NAV_ALL : NAV_ALL.slice(0, 1); + const isActive = (href: string) => loc.pathname === href || (href !== "/" && loc.pathname.startsWith(href)); return ( @@ -64,18 +71,25 @@ function MobileNav(props: { loggedIn: boolean }) { function TickerSearch(props: { loggedIn: boolean }) { const nav = useNavigate(); const [q, setQ] = createSignal(""); - const go = () => { - const t = q().toUpperCase().trim(); + const go = (t: string) => { + const v = t.toUpperCase().trim(); // Report butuh login: arahkan yg belum login ke /login. - if (t) nav(props.loggedIn ? `/report/${t}` : "/login"); + if (v) nav(props.loggedIn ? `/report/${v}` : "/login"); }; return ( -
{ e.preventDefault(); go(); }}> - - setQ(e.currentTarget.value)} /> - - + { e.preventDefault(); go(q()); }}> +
+ setQ(e.currentTarget.value)} + onKeyDown={(e: KeyboardEvent) => { if (e.key === "Enter") { e.preventDefault(); go(q()); } }} + class="h-9 pr-8" + /> + 🔍 +
+
); } @@ -99,7 +113,11 @@ function LoginPage() { finally { setBusy(false); } } return ( -
+
+
+ 📈 + FlowSight +
Masuk ke FlowSight 👋 diff --git a/web/src/pages/Alerts.tsx b/web/src/pages/Alerts.tsx index f0040bf..a9e32d9 100644 --- a/web/src/pages/Alerts.tsx +++ b/web/src/pages/Alerts.tsx @@ -50,6 +50,11 @@ function AlertsInner() { refetchDests(); } catch (e) { setDerr(String(e)); } } + const fmtDate = (s: unknown) => { + const t = String(s || ""); + const d = new Date(t); + return Number.isNaN(d.getTime()) ? t : d.toLocaleString("id-ID", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" }); + }; async function toggleDest(id: number, enabled: boolean) { try { await api.updateDestination(id, { enabled: !enabled }); refetchDests(); } catch (e) { setDerr(String(e)); } } async function delDest(id: number) { try { await api.deleteDestination(id); refetchDests(); } catch (e) { setDerr(String(e)); } } return ( @@ -111,7 +116,11 @@ function AlertsInner() { Notifikasi terpasang }> -
    {(a) =>
  • {a.name} terakhir: {a.last_fired || "belum pernah"}
  • }
+
    {(a) =>
  • + {a.name} + terakhir: {a.last_fired ? fmtDate(a.last_fired) : "belum pernah"} + +
  • }
@@ -122,7 +131,7 @@ function AlertsInner() { Belum ada kejadian.

}> -
    {(e) =>
  • {String(e.ticker)} {String(e.message)}
  • }
+
    {(e) =>
  • {String(e.ticker)}{String(e.message)}{fmtDate(e.date)}
  • }
diff --git a/web/src/pages/Dashboard.tsx b/web/src/pages/Dashboard.tsx index 65f7184..be7718c 100644 --- a/web/src/pages/Dashboard.tsx +++ b/web/src/pages/Dashboard.tsx @@ -38,13 +38,20 @@ export default function Dashboard() { }; const sorotan = () => (screen()?.rows || []).map((r) => ({ row: r, ...verdictFor(r) })); const closeMap = () => Object.fromEntries((flow()?.closes || []).map((c) => [c.ticker, c])); + // Resolve theme-aware primary color (falls back to blue) so the chart + // matches light/dark instead of a hardcoded hex. + const primaryColor = () => { + const c = getComputedStyle(document.documentElement).getPropertyValue("--primary"); + const [h, s, l] = c.trim().split(/[\s,]+/).map(Number); + return Number.isFinite(h) && Number.isFinite(s) && Number.isFinite(l) ? `hsl(${h} ${s}% ${l}%)` : "#3b82f6"; + }; const chartData = () => ({ labels: (foreign()?.dates || []) as string[], datasets: [{ label: `${chartTiker()} — uang asing harian (Rp juta)`, data: (foreign()?.nets || []) as number[], - borderColor: "#3b82f6", - backgroundColor: "rgba(59,130,246,.15)", + borderColor: primaryColor(), + backgroundColor: "transparent", fill: true, tension: 0.25, }], @@ -99,10 +106,15 @@ export default function Dashboard() { return ( - - {s.row.symbol} - {fmtHarga(cx.close)} - +
+ + {s.row.symbol} + {fmtHarga(cx.close)} + + +

{s.row.name}

+
+
diff --git a/web/src/pages/Portfolio.tsx b/web/src/pages/Portfolio.tsx index ee971e7..5fbf176 100644 --- a/web/src/pages/Portfolio.tsx +++ b/web/src/pages/Portfolio.tsx @@ -31,9 +31,22 @@ function PortfolioInner() { if (top.weight > 0.4) return `${top.ticker} porsinya ${(top.weight * 100).toFixed(0)}% — kebanyakan telur di satu keranjang. Pertimbangkan tambah saham beda sektor.`; return "Sebarannya sudah lumayan — tidak numpuk di satu tempat. Pertahankan. 👍"; }; + const primaryColor = () => { + const c = getComputedStyle(document.documentElement).getPropertyValue("--primary"); + const [h, s, l] = c.trim().split(/[\s,]+/).map(Number); + return Number.isFinite(h) && Number.isFinite(s) && Number.isFinite(l) ? `hsl(${h} ${s}% ${l}%)` : "#3b82f6"; + }; const chartData = () => ({ labels: conc().map((c) => c.ticker), - datasets: [{ label: "Porsi %", data: conc().map((c) => +(c.weight * 100).toFixed(1)) }], + datasets: [{ + label: "Porsi %", + data: conc().map((c) => +(c.weight * 100).toFixed(1)), + backgroundColor: conc().map((_, i) => { + const alphas = ["88", "66", "44", "33", "22"]; + return `${primaryColor()}${alphas[i % alphas.length]}`; + }), + borderRadius: 4, + }], }); return (
diff --git a/web/src/pages/Routines.tsx b/web/src/pages/Routines.tsx index 8695840..f8eb3a0 100644 --- a/web/src/pages/Routines.tsx +++ b/web/src/pages/Routines.tsx @@ -30,6 +30,13 @@ function RoutinesInner() { catch (e) { setErr(String(e)); } finally { setBusy(false); } } + const fmtTime = (s: unknown) => { + const t = String(s || ""); + if (!t) return "—"; + // ISO timestamp → "16 Sep 07:30" (local time). + const d = new Date(t); + return Number.isNaN(d.getTime()) ? t : d.toLocaleString("id-ID", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" }); + }; async function toggle(id: number, enabled: boolean) { try { await api.updateRoutine(id, { enabled: !enabled }); refetch(); } catch (e) { setErr(String(e)); } } @@ -93,7 +100,17 @@ function RoutinesInner() { -
    {(r) =>
  • {String(r.started_at)} — {String(r.status)}
  • }
+
    + {(r) => ( +
  • + + {String(r.status)} + {fmtTime(r.started_at)} + + {r.credits_used ? `${String(r.credits_used)} kredit` : `#${String(r.routine_id)}`} +
  • + )}
    +