import { useActionMutation, useActionQuery, } from "@agent-native/core/client/hooks"; import { useT } from "@agent-native/core/client/i18n"; import { useSetPageTitle } from "@agent-native/toolkit/app-shell"; import { IconPlus, IconStar, IconStarFilled, IconTrash } from "@tabler/icons-react"; import { useMemo, useState } from "react"; import { toast } from "sonner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { APP_TITLE } from "@/lib/app-config"; import { cn } from "@/lib/utils"; export function meta() { return [{ title: `Stocks - ${APP_TITLE}` }]; } interface StockFormState { symbol: string; companyName: string; exchange: string; sector: string; currency: string; price: string; changePercent: string; marketCap: string; } const EMPTY_FORM: StockFormState = { symbol: "", companyName: "", exchange: "NASDAQ", sector: "", currency: "USD", price: "", changePercent: "", marketCap: "", }; function formatMoney(value: number, currency: string) { try { return new Intl.NumberFormat(undefined, { style: "currency", currency, maximumFractionDigits: 2, }).format(value); } catch { return `${value.toFixed(2)} ${currency}`; } } function formatMarketCap(value: number | null) { if (value === null || value === undefined) return "—"; if (value >= 1_000_000_000_000) return `${(value / 1_000_000_000_000).toFixed(2)}T`; if (value >= 1_000_000_000) return `${(value / 1_000_000_000).toFixed(2)}B`; if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(2)}M`; return String(value); } function AddStockDialog() { const [open, setOpen] = useState(false); const [form, setForm] = useState(EMPTY_FORM); const addStock = useActionMutation("add-stock", { onSuccess: () => { toast.success(`${form.symbol.toUpperCase()} added`); setForm(EMPTY_FORM); setOpen(false); }, onError: (error) => toast.error(error.message), }); function submit() { if (!form.symbol.trim() || !form.companyName.trim()) { toast.error("Symbol and company name are required."); return; } addStock.mutate({ symbol: form.symbol.trim(), companyName: form.companyName.trim(), exchange: form.exchange.trim() || "NASDAQ", sector: form.sector.trim() || undefined, currency: form.currency.trim() || "USD", price: form.price ? Number(form.price) : 0, changePercent: form.changePercent ? Number(form.changePercent) : 0, marketCap: form.marketCap ? Number(form.marketCap) : undefined, }); } return ( Add stock
setForm({ ...form, symbol: e.target.value })} />
setForm({ ...form, companyName: e.target.value })} />
setForm({ ...form, exchange: e.target.value })} />
setForm({ ...form, sector: e.target.value })} />
setForm({ ...form, price: e.target.value })} />
setForm({ ...form, changePercent: e.target.value }) } />
setForm({ ...form, currency: e.target.value })} />
setForm({ ...form, marketCap: e.target.value })} />
); } interface StockRow { id: string; symbol: string; companyName: string; exchange: string; sector: string | null; currency: string; price: number; changePercent: number; marketCap: number | null; watchlisted: boolean; } function ChangeBadge({ value }: { value: number }) { const positive = value > 0; const negative = value < 0; return ( {positive ? "+" : ""} {value.toFixed(2)}% ); } function WatchlistButton({ stock }: { stock: StockRow }) { const toggle = useActionMutation("update-stock", { method: "PUT", onError: (error) => toast.error(error.message), }); return ( ); } function DeleteStockButton({ stock }: { stock: StockRow }) { const del = useActionMutation("delete-stock", { method: "DELETE", onSuccess: () => toast.success(`${stock.symbol} removed`), onError: (error) => toast.error(error.message), }); return ( ); } export default function StocksRoute() { const t = useT(); useSetPageTitle(t("navigation.stocks")); const [search, setSearch] = useState(""); const { data, isLoading } = useActionQuery("list-stocks", { search: search || undefined, }); const stocks = useMemo(() => (data?.stocks ?? []) as StockRow[], [data]); return (

{t("navigation.stocks")}

{t("pages.stocksDescription")}

setSearch(e.target.value)} className="sm:max-w-xs" /> {isLoading ? (

Loading…

) : stocks.length === 0 ? ( No stocks listed yet. Add one, or ask the agent to add some. ) : ( <> {/* Mobile: stacked cards */}
{stocks.map((stock) => (
{stock.symbol} {stock.exchange}

{stock.companyName}

{formatMoney(stock.price, stock.currency)}
))}
{/* Desktop: table */}
Symbol Company Exchange Sector Price Change Market Cap {stocks.map((stock) => ( {stock.symbol} {stock.companyName} {stock.exchange} {stock.sector ?? "—"} {formatMoney(stock.price, stock.currency)} {formatMarketCap(stock.marketCap)} ))}
)}
); }