feat: FE solid-ui (shadcn port) — Tailwind + 14 components, all pages rewritten
This commit is contained in:
+63
-31
@@ -3,6 +3,9 @@ import { Router, Route, useLocation, useNavigate, useParams, type RouteSectionPr
|
||||
import { createSignal, Show } from "solid-js";
|
||||
import { WatchlistDrawer, ChatSidebar } from "./components/WatchlistChat";
|
||||
import { ThemeToggle, useAuth, AuthButton } from "./components/auth";
|
||||
import { Button } from "./components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./components/ui/card";
|
||||
import { TextField, TextFieldInput } from "./components/ui/text-field";
|
||||
import { api } from "./lib/api";
|
||||
import Dashboard from "./pages/Dashboard";
|
||||
import Routines from "./pages/Routines";
|
||||
@@ -10,24 +13,47 @@ import Screener from "./pages/Screener";
|
||||
import Alerts from "./pages/Alerts";
|
||||
import Report from "./pages/Report";
|
||||
import Portfolio from "./pages/Portfolio";
|
||||
import "./styles/tokens.css";
|
||||
import "./styles/globals.css";
|
||||
|
||||
const NAV = [
|
||||
{ href: "/", icon: "📊", label: "Dashboard" },
|
||||
{ href: "/routines", icon: "🗓️", label: "Routines" },
|
||||
{ href: "/screener", icon: "🔍", label: "Screener" },
|
||||
{ href: "/alerts", icon: "🔔", label: "Alerts" },
|
||||
{ href: "/portfolio", icon: "💼", label: "Portfolio" },
|
||||
];
|
||||
|
||||
function Nav() {
|
||||
const loc = useLocation();
|
||||
const link = (href: string, icon: string, label: string) => (
|
||||
<a href={href} class={loc.pathname === href ? "active" : ""}><span>{icon}</span> {label}</a>
|
||||
);
|
||||
return (
|
||||
<nav class="fs-nav" id="fs-nav">
|
||||
<div class="fs-brand">Flow<span>Sight</span></div>
|
||||
{link("/", "📊", "Dashboard")}
|
||||
{link("/routines", "🗓️", "Routines")}
|
||||
{link("/screener", "🔍", "Screener")}
|
||||
{link("/alerts", "🔔", "Alerts")}
|
||||
{link("/portfolio", "💼", "Portfolio")}
|
||||
<div class="fs-nav-foot">
|
||||
<ThemeToggle />
|
||||
<nav class="flex w-56 shrink-0 flex-col gap-1 border-r bg-card p-4 max-lg:hidden">
|
||||
<div class="mb-4 px-2 text-xl font-bold tracking-tight">
|
||||
Flow<span class="text-primary">Sight</span>
|
||||
</div>
|
||||
{NAV.map((n) => (
|
||||
<a
|
||||
href={n.href}
|
||||
class={`flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground ${loc.pathname === n.href ? "bg-accent text-accent-foreground" : "text-muted-foreground"}`}
|
||||
>
|
||||
<span>{n.icon}</span> {n.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileNav() {
|
||||
const loc = useLocation();
|
||||
return (
|
||||
<nav class="fixed inset-x-0 bottom-0 z-40 flex border-t bg-card px-2 py-1 lg:hidden">
|
||||
{NAV.map((n) => (
|
||||
<a
|
||||
href={n.href}
|
||||
class={`flex flex-1 flex-col items-center rounded-md py-1.5 text-[11px] font-medium ${loc.pathname === n.href ? "text-primary" : "text-muted-foreground"}`}
|
||||
>
|
||||
<span class="text-base">{n.icon}</span> {n.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -40,10 +66,12 @@ function TickerSearch() {
|
||||
if (t) nav(`/report/${t}`);
|
||||
};
|
||||
return (
|
||||
<form class="fs-search" onSubmit={(e) => { e.preventDefault(); go(); }}>
|
||||
<input class="fs-input" placeholder="Ticker → report (e.g. BBCA)" value={q()}
|
||||
onInput={(e) => setQ(e.currentTarget.value)} style={{ flex: 1 }} />
|
||||
<button class="fs-btn" type="submit">Open</button>
|
||||
<form class="flex max-w-sm flex-1 items-center gap-2" onSubmit={(e) => { e.preventDefault(); go(); }}>
|
||||
<TextField class="flex-1">
|
||||
<TextFieldInput placeholder="Ticker → report (e.g. BBCA)" value={q()}
|
||||
onInput={(e) => setQ(e.currentTarget.value)} />
|
||||
</TextField>
|
||||
<Button type="submit" variant="secondary">Open</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -53,13 +81,17 @@ function LoginPage() {
|
||||
const err = () => new URLSearchParams(window.location.search).get("error");
|
||||
void params;
|
||||
return (
|
||||
<div style={{ "max-width": "440px", margin: "8vh auto" }}>
|
||||
<div class="fs-card" style={{ padding: "28px", "text-align": "center" }}>
|
||||
<h1 style={{ margin: "0 0 8px" }}>Welcome to FlowSight</h1>
|
||||
<p class="fs-muted">Sign in with Google to get your own watchlist, routines, alerts and reports.</p>
|
||||
<Show when={err()}><p style={{ color: "var(--fs-avoid)" }}>Login failed: {err()}</p></Show>
|
||||
<a class="fs-btn" href="/api/auth/start" style={{ display: "inline-block", "margin-top": "12px" }}>Sign in with Google</a>
|
||||
</div>
|
||||
<div class="mx-auto max-w-md py-[8vh]">
|
||||
<Card>
|
||||
<CardHeader class="text-center">
|
||||
<CardTitle class="text-2xl">Welcome to FlowSight</CardTitle>
|
||||
<CardDescription>Sign in with Google to get your own watchlist, routines, alerts and reports.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="text-center">
|
||||
<Show when={err()}><p class="mb-3 text-sm text-destructive">Login failed: {err()}</p></Show>
|
||||
<a href="/api/auth/start"><Button size="lg">Sign in with Google</Button></a>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -68,22 +100,22 @@ function Shell(props: RouteSectionProps) {
|
||||
const { me, refetch } = useAuth();
|
||||
const logout = async () => { await api.logout(); refetch(); };
|
||||
return (
|
||||
<div class="fs-app">
|
||||
<div class="flex min-h-screen bg-background text-foreground">
|
||||
<Nav />
|
||||
<div class="fs-body">
|
||||
<header class="fs-topbar">
|
||||
<button id="fs-menu-btn" class="fs-iconbtn" onClick={() => document.getElementById("fs-nav")?.classList.toggle("open")}>☰</button>
|
||||
<div class="flex min-w-0 flex-1 flex-col">
|
||||
<header class="sticky top-0 z-30 flex items-center gap-3 border-b bg-background/95 px-4 py-2.5 backdrop-blur">
|
||||
<TickerSearch />
|
||||
<span class="fs-spacer" />
|
||||
<span class="flex-1" />
|
||||
<ThemeToggle />
|
||||
<AuthButton me={me()} onLogout={logout} />
|
||||
</header>
|
||||
<main class="fs-main">{props.children}</main>
|
||||
<main class="mx-auto w-full max-w-6xl flex-1 space-y-6 p-4 pb-20 lg:p-6">{props.children}</main>
|
||||
</div>
|
||||
<aside class="fs-side">
|
||||
<aside class="hidden w-80 shrink-0 flex-col gap-4 border-l bg-card p-4 xl:flex">
|
||||
<WatchlistDrawer />
|
||||
<ChatSidebar />
|
||||
</aside>
|
||||
<MobileNav />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user