From ff156112e75ff00eec06d1354a9bfbd13e326dbd Mon Sep 17 00:00:00 2001 From: asepharyana Date: Tue, 15 Sep 2026 16:35:03 +0700 Subject: [PATCH] feat: light/dark theme + app shell (sidebar, topbar, auth, login page) --- web/src/components/auth.tsx | 49 +++++++++++++ web/src/components/ui.tsx | 66 ++++++++++++++++++ web/src/index.tsx | 94 +++++++++++++++++++------ web/src/lib/api.ts | 5 +- web/src/styles/tokens.css | 135 +++++++++++++++++++++++++++++++----- 5 files changed, 309 insertions(+), 40 deletions(-) create mode 100644 web/src/components/auth.tsx create mode 100644 web/src/components/ui.tsx diff --git a/web/src/components/auth.tsx b/web/src/components/auth.tsx new file mode 100644 index 0000000..f0f12c6 --- /dev/null +++ b/web/src/components/auth.tsx @@ -0,0 +1,49 @@ +import { createResource, createSignal, Show } from "solid-js"; +import { api, type AuthUser } from "../lib/api"; + +// AuthState: loads /api/auth/me once; exposes login/logout + theme toggle. +function theme(): string { + return localStorage.getItem("fs-theme") || + (matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark"); +} + +export function applyTheme(t: string) { + document.documentElement.dataset.theme = t; + localStorage.setItem("fs-theme", t); +} +applyTheme(theme()); + +export function ThemeToggle() { + const [t, setT] = createSignal(theme()); + return ( + + ); +} + +export function useAuth() { + const [me, { refetch }] = createResource(async (): Promise => { + try { return (await api.me()).user; } catch { return null; } + }); + return { me, refetch }; +} + +export function AuthButton(props: { me: AuthUser | null | undefined; onLogout: () => void }) { + return ( + Sign in with Google + }> + {(u) => ( + + {u().avatar_url ? : null} + {u().name || u().email} + + + )} + + ); +} diff --git a/web/src/components/ui.tsx b/web/src/components/ui.tsx new file mode 100644 index 0000000..823f668 --- /dev/null +++ b/web/src/components/ui.tsx @@ -0,0 +1,66 @@ +import { For, Show, type JSX } from "solid-js"; + +// Shared building blocks: StatCard, EmptyState, Badge, SignalBar, Skeleton. + +export function StatCard(props: { title: string; value: string; sub?: string; children?: JSX.Element }) { + return ( +
+

{props.title}

+

{props.value}

+

{props.sub}

+ {props.children} +
+ ); +} + +export function EmptyState(props: { icon: string; title: string; hint?: string; action?: JSX.Element }) { + return ( +
+
{props.icon}
+ {props.title} +

{props.hint}

+
{props.action}
+
+ ); +} + +export function Skeleton(props: { rows?: number }) { + const n = props.rows ?? 3; + return ( +
+ {() =>
} +
+ ); +} + +export function RecBadge(props: { rec: string }) { + const cls = props.rec === "BUY" ? "buy" : props.rec === "HOLD" ? "hold" : "avoid"; + return {props.rec}; +} + +// SignalBar renders one -100..+100 signal as a labeled bar. +export function SignalBar(props: { label: string; value: number }) { + const v = Math.max(-100, Math.min(100, props.value)); + const left = v < 0 ? `${50 + v / 2}%` : "50%"; + const width = `${Math.abs(v) / 2}%`; + const cls = v < 0 ? "neg" : v > 0 ? "pos" : ""; + return ( +
+ {props.label} + + {v} +
+ ); +} + +// BreakdownBars renders a screen/report signal breakdown map as bars. +// Unknown numeric fields are shown; booleans/strings are skipped except flags. +export function BreakdownBars(props: { breakdown: Record }) { + const entries = () => + Object.entries(props.breakdown || {}).filter(([, v]) => typeof v === "number") as [string, number][]; + return ( + no signals}> + {([k, v]) => } + + ); +} diff --git a/web/src/index.tsx b/web/src/index.tsx index ea28da7..d0ef895 100644 --- a/web/src/index.tsx +++ b/web/src/index.tsx @@ -1,6 +1,9 @@ import { render } from "solid-js/web"; -import { Router, Route, useLocation } from "@solidjs/router"; +import { Router, Route, useLocation, useNavigate, useParams, type RouteSectionProps } from "@solidjs/router"; +import { createSignal, Show } from "solid-js"; import { WatchlistDrawer, ChatSidebar } from "./components/WatchlistChat"; +import { ThemeToggle, useAuth, AuthButton } from "./components/auth"; +import { api } from "./lib/api"; import Dashboard from "./pages/Dashboard"; import Routines from "./pages/Routines"; import Screener from "./pages/Screener"; @@ -11,39 +14,90 @@ import "./styles/tokens.css"; function Nav() { const loc = useLocation(); - const link = (href: string, label: string) => ( - {label} + const link = (href: string, icon: string, label: string) => ( + {icon} {label} ); return ( -