Function bodies 201 total
DividendsPage function · typescript · L13-L48 (36 LOC)src/app/dashboard/dividends/page.tsx
export default async function DividendsPage() {
const session = await auth();
if (!session?.user?.id) redirect("/login");
const currency = session.user.defaultCurrency ?? "USD";
const data = await getDividendData(session.user.id);
return (
<main className="container py-8 space-y-6">
<PageHeader
icon={DollarSign}
title="Dividends"
description="Track your dividend income and payment history."
/>
<DividendSummaryCards
totalDividends={data.totalDividends}
monthlyAverage={data.monthlyAverage}
last12Months={data.last12Months}
totalCount={data.totalCount}
currency={currency}
/>
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
<div className="lg:col-span-3">
<DividendChart monthly={data.monthly} currency={currency} />
</div>
<div className="lg:col-span-2">
<DividendByAssetChart byAsset={data.byAsset} currency={currency} />
</DashboardLayout function · typescript · L6-L34 (29 LOC)src/app/dashboard/layout.tsx
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await auth();
if (!session?.user?.id) redirect("/login");
return (
<div className="min-h-screen bg-background">
<SidebarNav
userName={session.user.name ?? session.user.email ?? ""}
userEmail={session.user.email ?? undefined}
/>
<OnboardingDialog userId={session.user.id} />
<div className="relative pt-14 md:pt-0 md:pl-64">
{/* Subtle dot-grid texture */}
<div
className="pointer-events-none absolute inset-0 opacity-[0.06]"
style={{
backgroundImage: "radial-gradient(circle, currentColor 1px, transparent 1px)",
backgroundSize: "24px 24px",
}}
/>
<div className="relative">{children}</div>
</div>
</div>
);
}DashboardLoading function · typescript · L3-L26 (24 LOC)src/app/dashboard/loading.tsx
export default function DashboardLoading() {
return (
<div className="container py-8 space-y-8">
{/* Summary cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="h-28 rounded-xl" />
))}
</div>
{/* Charts */}
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
<Skeleton className="h-80 lg:col-span-3 rounded-xl" />
<Skeleton className="h-80 lg:col-span-2 rounded-xl" />
</div>
{/* Top movers */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Skeleton className="h-48 rounded-xl" />
<Skeleton className="h-48 rounded-xl" />
</div>
</div>
);
}DashboardPage function · typescript · L15-L62 (48 LOC)src/app/dashboard/page.tsx
export default async function DashboardPage() {
const session = await auth();
if (!session?.user?.id) redirect("/login");
const currency = session.user.defaultCurrency ?? "USD";
const [data, triggeredAlerts] = await Promise.all([
getDashboardData(session.user.id, currency),
checkAndFireAlerts(session.user.id),
]);
return (
<main className="container py-8 space-y-6">
<AlertNotifier triggered={triggeredAlerts} />
<PageHeader
icon={LayoutDashboard}
title="Dashboard"
description={`Welcome back, ${session.user.name ?? "User"}.`}
/>
<SummaryCards
totalValue={data.totalValue}
totalCost={data.totalCost}
totalGainLoss={data.totalGainLoss}
totalGainLossPct={data.totalGainLossPct}
currency={currency}
/>
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
<div className="lg:col-span-3">
<PerformanceChart performance={data.performance} />
</diSettingsLoading function · typescript · L3-L33 (31 LOC)src/app/dashboard/settings/loading.tsx
export default function SettingsLoading() {
return (
<div className="container py-8 space-y-6 max-w-3xl">
{/* Page header */}
<Skeleton className="h-8 w-28" />
{/* Tab bar */}
<div className="flex gap-2">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} className="h-8 w-20" />
))}
</div>
{/* Form card */}
<div className="rounded-xl border p-6 space-y-5">
<Skeleton className="h-6 w-32" />
<div className="space-y-3">
<div className="space-y-1.5">
<Skeleton className="h-4 w-16" />
<Skeleton className="h-9 w-full" />
</div>
<div className="space-y-1.5">
<Skeleton className="h-4 w-16" />
<Skeleton className="h-9 w-full" />
</div>
</div>
<Skeleton className="h-9 w-24" />
</div>
</div>
);
}SettingsPage function · typescript · L11-L29 (19 LOC)src/app/dashboard/settings/page.tsx
export default async function SettingsPage() {
const session = await auth();
if (!session?.user?.id) redirect("/login");
const profile = await getUserProfile(session.user.id);
if (!profile) redirect("/login");
return (
<main className="container py-8 space-y-6">
<PageHeader
icon={Settings}
title="Settings"
description="Manage your account, preferences, and data."
/>
<UserGuidePanel />
<SettingsShell profile={profile} />
</main>
);
}SupportPage function · typescript · L9-L23 (15 LOC)src/app/dashboard/support/page.tsx
export default async function SupportPage() {
const session = await auth();
if (!session?.user?.id) redirect("/login");
return (
<main className="container py-8 space-y-6">
<PageHeader
icon={LifeBuoy}
title="Support"
description="Have a question or issue? Send us a message and we'll get back to you."
/>
<SupportForm />
</main>
);
}All rows scored by the Repobility analyzer (https://repobility.com)
TransactionsLoading function · typescript · L3-L40 (38 LOC)src/app/dashboard/transactions/loading.tsx
export default function TransactionsLoading() {
return (
<div className="container py-8 space-y-6">
{/* Page header */}
<Skeleton className="h-8 w-44" />
{/* Filter bar */}
<div className="flex flex-wrap gap-2">
<Skeleton className="h-9 w-36" />
<Skeleton className="h-9 w-36" />
<Skeleton className="h-9 w-32" />
<Skeleton className="h-9 w-32" />
</div>
{/* Action bar */}
<div className="flex justify-between">
<Skeleton className="h-4 w-32" />
<div className="flex gap-2">
<Skeleton className="h-9 w-9" />
<Skeleton className="h-9 w-28" />
<Skeleton className="h-9 w-36" />
</div>
</div>
{/* Table rows */}
<div className="rounded-md border p-4 space-y-3">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="flex gap-4">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-4 w-16TransactionsPage function · typescript · L12-L63 (52 LOC)src/app/dashboard/transactions/page.tsx
export default async function TransactionsPage() {
const session = await auth();
if (!session?.user?.id) redirect("/login");
const currency = session.user.defaultCurrency ?? "USD";
const [dbTransactions, dbAssets] = await Promise.all([
getTransactionsByUser(session.user.id),
getAssetsByUser(session.user.id),
]);
const enrichedTransactions: EnrichedTransaction[] = dbTransactions.map((t) => ({
id: t.id,
type: t.type,
quantity: t.quantity,
pricePerUnit: t.pricePerUnit,
fees: t.fees,
total:
t.type === "BUY"
? t.quantity * t.pricePerUnit + t.fees
: t.quantity * t.pricePerUnit - t.fees,
date: t.date.toISOString(),
notes: t.notes,
assetId: t.assetId,
assetSymbol: t.asset.symbol,
assetName: t.asset.name,
portfolioId: t.asset.portfolioId,
portfolioName: t.asset.portfolio.name,
}));
const assetOptions: AssetOption[] = dbAssets.map((a) => ({
id: a.id,
symbol: a.symbol,
name: a.name,
WatchlistPage function · typescript · L12-L54 (43 LOC)src/app/dashboard/watchlist/page.tsx
export default async function WatchlistPage() {
const session = await auth();
if (!session?.user?.id) redirect("/login");
const dbItems = await getWatchlistByUser(session.user.id);
const marketSymbols = dbItems.map((item) =>
toMarketSymbol(item.symbol, item.assetType)
);
const quotes =
dbItems.length > 0
? await getBatchQuotes(marketSymbols)
: new Map<string, null>();
const items: WatchlistRow[] = dbItems.map((item, i) => {
const mSym = marketSymbols[i].toUpperCase();
const quote = quotes.get(mSym) ?? null;
return {
id: item.id,
symbol: item.symbol,
name: item.name,
assetType: item.assetType,
notes: item.notes,
addedAt: item.addedAt.toISOString(),
currentPrice: quote?.price ?? null,
change: quote?.change ?? null,
changePercent: quote?.changePercent ?? null,
currency: quote?.currency ?? "USD",
};
});
return (
<main className="container py-8 space-y-6">
<PageHeRootLayout function · typescript · L15-L30 (16 LOC)src/app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
</head>
<body className={inter.className}>
<Providers>{children}</Providers>
</body>
</html>
);
}LegalLayout function · typescript · L5-L35 (31 LOC)src/app/(legal)/layout.tsx
export default function LegalLayout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen bg-background">
<header className="border-b">
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
<Link href="/" className="flex items-center gap-2">
<FolioVaultLogo size={20} className="text-primary" />
<span className="font-semibold tracking-tight">FolioVault</span>
</Link>
<Link
href="/"
className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<ArrowLeft className="h-4 w-4" />
Back
</Link>
</div>
</header>
<main className="container mx-auto px-4 py-12 max-w-3xl">
{children}
</main>
<footer className="border-t">
<div className="container mx-auto px-4 py-6 text-center text-sm text-muted-foreground"PrivacyPage function · typescript · L8-L117 (110 LOC)src/app/(legal)/privacy/page.tsx
export default function PrivacyPage() {
return (
<article className="prose prose-neutral dark:prose-invert max-w-none">
<h1>Privacy Policy</h1>
<p className="text-muted-foreground">Last updated: February 23, 2026</p>
<p>
Your privacy matters to us. This Privacy Policy explains how FolioVault
collects, uses, stores, and protects your information when you use our
service.
</p>
<h2>1. Information We Collect</h2>
<h3>Account Information</h3>
<p>
When you create an account, we collect your name, email address, and
password (stored in hashed form). If you sign in with a third-party
provider (e.g. Google), we receive your name, email, and profile image
from that provider.
</p>
<h3>Portfolio Data</h3>
<p>
You may submit portfolio information including asset names, quantities,
transaction records, and related financial data. This data is provided
voTermsPage function · typescript · L8-L114 (107 LOC)src/app/(legal)/terms/page.tsx
export default function TermsPage() {
return (
<article className="prose prose-neutral dark:prose-invert max-w-none">
<h1>Terms of Service</h1>
<p className="text-muted-foreground">Last updated: February 23, 2026</p>
<p>
Welcome to FolioVault. By accessing or using our service, you agree to
be bound by these Terms of Service ("Terms"). If you do not
agree, please do not use FolioVault.
</p>
<h2>1. Account Responsibilities</h2>
<p>
You are responsible for maintaining the confidentiality of your account
credentials. You agree to provide accurate, current, and complete
information during registration and to update it as necessary. You are
responsible for all activity that occurs under your account.
</p>
<h2>2. Acceptable Use</h2>
<p>You agree not to:</p>
<ul>
<li>Use the service for any unlawful purpose or in violation of any applicable laws.</li>
Home function · typescript · L41-L127 (87 LOC)src/app/page.tsx
export default async function Home() {
const session = await auth();
if (session?.user?.id) redirect("/dashboard");
return (
<main className="min-h-screen bg-background">
{/* Hero */}
<section className="relative overflow-hidden">
{/* Gradient + dot-grid background */}
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-background to-primary/5" />
<div
className="absolute inset-0 opacity-[0.15]"
style={{
backgroundImage:
"radial-gradient(circle, hsl(var(--primary)) 1px, transparent 1px)",
backgroundSize: "24px 24px",
}}
/>
<div className="relative container mx-auto px-4 py-32 md:py-44 flex flex-col items-center text-center">
{/* Logo pill */}
<div className="inline-flex items-center gap-2 rounded-full border bg-background/80 backdrop-blur px-4 py-1.5 mb-6 text-sm font-medium">
<FolioVaultLogo size={16} clasRepobility analyzer · published findings · https://repobility.com
AddAlertDialog function · typescript · L41-L196 (156 LOC)src/components/alerts/add-alert-dialog.tsx
export function AddAlertDialog({
open,
onOpenChange,
assetOptions,
onSuccess,
}: Props) {
const [form, setForm] = useState<FormState>(defaultForm);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!open) return;
setForm(defaultForm());
setError(null);
}, [open]);
function set<K extends keyof FormState>(key: K, value: FormState[K]) {
setForm((prev) => ({ ...prev, [key]: value }));
}
function handleAssetChange(assetId: string) {
const asset = assetOptions.find((a) => a.id === assetId);
setForm((prev) => ({
...prev,
assetId,
symbol: asset?.symbol ?? "",
}));
}
const price = parseFloat(form.targetPrice);
const previewValid = form.symbol && !isNaN(price) && price > 0;
const directionLabel = form.condition === "ABOVE" ? "goes above" : "falls below";
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setErrhandleAssetChange function · typescript · L61-L68 (8 LOC)src/components/alerts/add-alert-dialog.tsx
function handleAssetChange(assetId: string) {
const asset = assetOptions.find((a) => a.id === assetId);
setForm((prev) => ({
...prev,
assetId,
symbol: asset?.symbol ?? "",
}));
}handleSubmit function · typescript · L74-L103 (30 LOC)src/components/alerts/add-alert-dialog.tsx
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError(null);
if (!form.assetId) return setError("Please select an asset.");
if (isNaN(price) || price <= 0)
return setError("Target price must be a positive number.");
setLoading(true);
const res = await fetch("/api/alerts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
assetId: form.assetId,
symbol: form.symbol,
condition: form.condition,
targetPrice: price,
}),
});
setLoading(false);
if (!res.ok) {
const body = await res.json().catch(() => ({}));
setError((body as { error?: string }).error ?? "Something went wrong.");
return;
}
onSuccess();
onOpenChange(false);
}AlertNotifier function · typescript · L11-L25 (15 LOC)src/components/alerts/alert-notifier.tsx
export function AlertNotifier({ triggered }: AlertNotifierProps) {
useEffect(() => {
triggered.forEach((a) => {
const direction = a.condition === "ABOVE" ? "above" : "below";
toast({
title: `Price alert triggered: ${a.symbol}`,
description: `${a.symbol} is now $${a.currentPrice.toFixed(2)} — ${direction} your target of $${a.targetPrice.toFixed(2)}`,
variant: "success",
});
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return null;
}AlertsTable function · typescript · L27-L186 (160 LOC)src/components/alerts/alerts-table.tsx
export function AlertsTable({ initialAlerts, assetOptions }: Props) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const [addOpen, setAddOpen] = useState(false);
const [alerts, setAlerts] = useState(initialAlerts);
function refresh() {
startTransition(() => router.refresh());
}
async function handleDelete(id: string) {
setAlerts((prev) => prev.filter((a) => a.id !== id));
await fetch(`/api/alerts/${id}`, { method: "DELETE" });
refresh();
}
async function handleReactivate(id: string) {
setAlerts((prev) =>
prev.map((a) =>
a.id === id ? { ...a, active: true, triggeredAt: null } : a
)
);
await fetch(`/api/alerts/${id}`, { method: "PATCH" });
refresh();
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">
{alerts.length} alert{alerts.length !== 1 ? "s" : ""}
<handleReactivate function · typescript · L43-L51 (9 LOC)src/components/alerts/alerts-table.tsx
async function handleReactivate(id: string) {
setAlerts((prev) =>
prev.map((a) =>
a.id === id ? { ...a, active: true, triggeredAt: null } : a
)
);
await fetch(`/api/alerts/${id}`, { method: "PATCH" });
refresh();
}SortIcon function · typescript · L29-L45 (17 LOC)src/components/analytics/asset-breakdown-table.tsx
function SortIcon({
field,
sortKey,
sortDir,
}: {
field: SortKey;
sortKey: SortKey;
sortDir: SortDir;
}) {
if (field !== sortKey)
return <ArrowUpDown className="h-3 w-3 ml-1 opacity-40" />;
return sortDir === "asc" ? (
<ArrowUp className="h-3 w-3 ml-1" />
) : (
<ArrowDown className="h-3 w-3 ml-1" />
);
}AssetBreakdownTable function · typescript · L73-L270 (198 LOC)src/components/analytics/asset-breakdown-table.tsx
export function AssetBreakdownTable({ assets, currency = "USD" }: Props) {
const [sortKey, setSortKey] = useState<SortKey>("gainLossPct");
const [sortDir, setSortDir] = useState<SortDir>("desc");
function handleSort(key: SortKey) {
if (key === sortKey) {
setSortDir((d) => (d === "asc" ? "desc" : "asc"));
} else {
setSortKey(key);
setSortDir("desc");
}
}
if (assets.length === 0) {
return (
<Card>
<CardHeader>
<CardTitle>Asset Breakdown</CardTitle>
</CardHeader>
<CardContent>
<EmptyState
icon={BarChart2}
title="No assets yet"
description="Add assets to your portfolios to see the breakdown."
/>
</CardContent>
</Card>
);
}
const sorted = [...assets].sort((a, b) => {
const dir = sortDir === "asc" ? 1 : -1;
switch (sortKey) {
case "symbol":
return dir * a.symbol.localeCompare(b.symbol);
case "value":
Source: Repobility analyzer · https://repobility.com
handleSort function · typescript · L77-L84 (8 LOC)src/components/analytics/asset-breakdown-table.tsx
function handleSort(key: SortKey) {
if (key === sortKey) {
setSortDir((d) => (d === "asc" ? "desc" : "asc"));
} else {
setSortKey(key);
setSortDir("desc");
}
}PnlTooltip function · typescript · L30-L47 (18 LOC)src/components/analytics/pnl-bar-chart.tsx
function PnlTooltip({ active, payload }: TooltipProps<number, string>) {
if (!active || !payload?.length) return null;
const d = payload[0].payload as TooltipEntry;
const positive = d.gainLoss >= 0;
return (
<div className="rounded-lg border bg-background p-2 shadow-sm text-sm space-y-1">
<p className="font-semibold font-mono">{d.symbol}</p>
<p className="text-xs text-muted-foreground">{d.name}</p>
<p className={positive ? "text-positive" : "text-negative"}>
{formatPercent(d.gainLossPct)}
</p>
<p className="text-muted-foreground text-xs">
P&L: {positive ? "+" : ""}
{formatCurrency(d.gainLoss)}
</p>
</div>
);
}PnlBarChart function · typescript · L49-L131 (83 LOC)src/components/analytics/pnl-bar-chart.tsx
export function PnlBarChart({ assets }: Props) {
if (assets.length === 0) {
return (
<Card>
<CardHeader>
<CardTitle>Return by Asset</CardTitle>
</CardHeader>
<CardContent>
<EmptyState
icon={BarChart2}
title="No assets"
description="Add assets to your portfolios to see P&L breakdown."
/>
</CardContent>
</Card>
);
}
const sorted = [...assets].sort((a, b) => b.gainLossPct - a.gainLossPct);
const data: TooltipEntry[] = sorted.map((a) => ({
...a,
pnlPct: +(a.gainLossPct * 100).toFixed(2),
}));
const barHeight = 36;
const chartHeight = Math.max(180, data.length * barHeight + 48);
return (
<Card>
<CardHeader>
<CardTitle>Return by Asset</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={chartHeight}>
<BarChart
data={data}
layout="vertical"
AssetAlertPanel function · typescript · L25-L177 (153 LOC)src/components/assets/asset-alert-panel.tsx
export function AssetAlertPanel({ assetId, symbol, initialAlerts }: Props) {
const [alerts, setAlerts] = useState<PriceAlertRow[]>(initialAlerts);
const [condition, setCondition] = useState<"ABOVE" | "BELOW">("ABOVE");
const [targetPrice, setTargetPrice] = useState("");
const [loading, setLoading] = useState(false);
async function handleCreate() {
const price = parseFloat(targetPrice);
if (!price || price <= 0) return;
setLoading(true);
try {
const res = await fetch("/api/alerts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ assetId, symbol, condition, targetPrice: price }),
});
if (res.ok) {
const alert = await res.json();
setAlerts((prev) => [
{
id: alert.id,
assetId: alert.assetId,
symbol: alert.symbol,
assetName: "",
condition: alert.condition,
targetPrice: alert.targetPrihandleCreate function · typescript · L31-L63 (33 LOC)src/components/assets/asset-alert-panel.tsx
async function handleCreate() {
const price = parseFloat(targetPrice);
if (!price || price <= 0) return;
setLoading(true);
try {
const res = await fetch("/api/alerts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ assetId, symbol, condition, targetPrice: price }),
});
if (res.ok) {
const alert = await res.json();
setAlerts((prev) => [
{
id: alert.id,
assetId: alert.assetId,
symbol: alert.symbol,
assetName: "",
condition: alert.condition,
targetPrice: alert.targetPrice,
active: alert.active,
triggeredAt: alert.triggeredAt ?? null,
createdAt: alert.createdAt,
},
...prev,
]);
setTargetPrice("");
}
} finally {
setLoading(false);
}
}handleReactivate function · typescript · L70-L79 (10 LOC)src/components/assets/asset-alert-panel.tsx
async function handleReactivate(id: string) {
const res = await fetch(`/api/alerts/${id}`, { method: "PATCH" });
if (res.ok) {
setAlerts((prev) =>
prev.map((a) =>
a.id === id ? { ...a, active: true, triggeredAt: null } : a
)
);
}
}AssetDetailView function · typescript · L40-L91 (52 LOC)src/components/assets/asset-detail-view.tsx
export function AssetDetailView({ asset, tvSymbol, alerts }: Props) {
return (
<main className="container py-8 space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
<Button variant="ghost" size="sm" asChild>
<Link href="/dashboard/assets">
<ArrowLeft className="h-4 w-4 mr-1" />
Back to Assets
</Link>
</Button>
<div className="flex items-center gap-2">
<h1 className="text-2xl font-bold tracking-tight">
{asset.symbol}
</h1>
<span className="text-muted-foreground text-lg">—</span>
<span className="text-lg text-muted-foreground">{asset.name}</span>
<span
className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${ASSET_TYPE_BADGE[asset.assetType] ?? "bg-muted text-muted-foreground"}`}
>
{ASSET_TYPE_LABELS[asset.assetType] ?? asset.assetType}
defaultForm function · typescript · L45-L56 (12 LOC)src/components/assets/asset-form-dialog.tsx
function defaultForm(asset?: EnrichedAsset): FormState {
return {
portfolioId: asset?.portfolioId ?? "",
symbol: asset?.symbol ?? "",
name: asset?.name ?? "",
assetType: asset?.assetType ?? "STOCK",
quantity: asset ? String(asset.quantity) : "",
averageBuyPrice: asset ? String(asset.averageBuyPrice) : "",
currency: asset?.currency ?? "USD",
notes: asset?.notes ?? "",
};
}Repobility — same analyzer, your code, free for public repos · /scan/
handleSelectResult function · typescript · L102-L111 (10 LOC)src/components/assets/asset-form-dialog.tsx
function handleSelectResult(r: SymbolSearchResult) {
setForm((prev) => ({
...prev,
symbol: r.symbol,
name: r.name || prev.name,
assetType: r.suggestedType || prev.assetType,
}));
setResults([]);
setDropdownOpen(false);
}handleCreatePortfolio function · typescript · L113-L138 (26 LOC)src/components/assets/asset-form-dialog.tsx
async function handleCreatePortfolio() {
if (!newPortfolioName.trim()) return;
setLoading(true);
setError(null);
try {
const res = await fetch("/api/portfolios", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: newPortfolioName.trim() }),
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
setError((body as { error?: string }).error ?? "Failed to create portfolio.");
setLoading(false);
return;
}
const portfolio = await res.json();
setPortfolioList((prev) => [...prev, { id: portfolio.id, name: portfolio.name }]);
set("portfolioId", portfolio.id);
setCreatingPortfolio(false);
setNewPortfolioName("");
} catch {
setError("Failed to create portfolio.");
}
setLoading(false);
}handleSubmit function · typescript · L140-L185 (46 LOC)src/components/assets/asset-form-dialog.tsx
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError(null);
const qty = parseFloat(form.quantity);
const abp = parseFloat(form.averageBuyPrice);
if (!form.symbol.trim()) return setError("Symbol is required.");
if (!form.name.trim()) return setError("Name is required.");
if (!form.assetType) return setError("Asset type is required.");
if (!isEdit && !form.portfolioId) return setError("Portfolio is required.");
if (isNaN(qty) || qty <= 0) return setError("Quantity must be a positive number.");
if (isNaN(abp) || abp <= 0) return setError("Average buy price must be positive.");
if (!form.currency.trim()) return setError("Currency is required.");
setLoading(true);
const payload = {
portfolioId: form.portfolioId,
symbol: form.symbol.trim().toUpperCase(),
name: form.name.trim(),
assetType: form.assetType,
quantity: qty,
averageBuyPrice: abp,
currency: form.currency.trimhandleSort function · typescript · L164-L171 (8 LOC)src/components/assets/assets-table.tsx
function handleSort(field: SortField) {
if (field === sortField) {
setSortDir((d) => (d === "asc" ? "desc" : "asc"));
} else {
setSortField(field);
setSortDir("asc");
}
}handleDelete function · typescript · L178-L188 (11 LOC)src/components/assets/assets-table.tsx
async function handleDelete() {
if (!pendingDelete) return;
setDeleteLoading(true);
await Promise.all(
pendingDelete.ids.map((id) => fetch(`/api/assets/${id}`, { method: "DELETE" }))
);
setDeleteLoading(false);
setAssets((prev) => prev.filter((a) => a.id !== pendingDelete.id));
setPendingDelete(null);
refresh();
}makeEditSuccessHandler function · typescript · L190-L197 (8 LOC)src/components/assets/assets-table.tsx
function makeEditSuccessHandler(asset: EnrichedAsset) {
if (asset.ids.length <= 1) return refresh;
return async () => {
const [, ...extraIds] = asset.ids;
await Promise.all(extraIds.map((id) => fetch(`/api/assets/${id}`, { method: "DELETE" })));
refresh();
};
}HoldingsSummaryCard function · typescript · L20-L76 (57 LOC)src/components/assets/holdings-summary-card.tsx
export function HoldingsSummaryCard({
symbol,
name,
assetType,
portfolioName,
quantity,
averageBuyPrice,
currentPrice,
marketValue,
pnl,
pnlPct,
}: Props) {
const positive = pnl >= 0;
const pnlClass = positive ? "text-positive" : "text-negative";
return (
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
Holdings Summary
<span
className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${ASSET_TYPE_BADGE[assetType] ?? "bg-muted text-muted-foreground"}`}
>
{ASSET_TYPE_LABELS[assetType] ?? assetType}
</span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm">
<Row label="Symbol" value={symbol} mono />
<Row label="Name" value={name} />
<Row label="Portfolio" value={portfolioName} />
<div className="border-t my-2" />
<Row label="Quantity" Row function · typescript · L78-L97 (20 LOC)src/components/assets/holdings-summary-card.tsx
function Row({
label,
value,
mono,
className,
}: {
label: string;
value: string;
mono?: boolean;
className?: string;
}) {
return (
<div className="flex justify-between">
<span className="text-muted-foreground">{label}</span>
<span className={`${mono ? "font-mono" : ""} ${className ?? ""}`}>
{value}
</span>
</div>
);
}All rows scored by the Repobility analyzer (https://repobility.com)
ManagePortfoliosDialog function · typescript · L25-L152 (128 LOC)src/components/assets/manage-portfolios-dialog.tsx
export function ManagePortfoliosDialog({
open,
onOpenChange,
portfolios,
assets,
onDeleted,
onRefresh,
}: Props) {
const [pendingDelete, setPendingDelete] = useState<PortfolioOption | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
async function handleDelete() {
if (!pendingDelete) return;
setLoading(true);
setError(null);
try {
const res = await fetch(`/api/portfolios/${pendingDelete.id}`, { method: "DELETE" });
if (!res.ok) {
const body = await res.json().catch(() => ({}));
setError((body as { error?: string }).error ?? "Failed to delete portfolio.");
setLoading(false);
return;
}
onDeleted(pendingDelete.id);
setPendingDelete(null);
onRefresh();
} catch {
setError("Failed to delete portfolio.");
}
setLoading(false);
}
return (
<>
<Dialog
open={open}
onOpenChange={(handleDelete function · typescript · L37-L56 (20 LOC)src/components/assets/manage-portfolios-dialog.tsx
async function handleDelete() {
if (!pendingDelete) return;
setLoading(true);
setError(null);
try {
const res = await fetch(`/api/portfolios/${pendingDelete.id}`, { method: "DELETE" });
if (!res.ok) {
const body = await res.json().catch(() => ({}));
setError((body as { error?: string }).error ?? "Failed to delete portfolio.");
setLoading(false);
return;
}
onDeleted(pendingDelete.id);
setPendingDelete(null);
onRefresh();
} catch {
setError("Failed to delete portfolio.");
}
setLoading(false);
}TradingViewChartInner function · typescript · L10-L96 (87 LOC)src/components/assets/tradingview-chart.tsx
function TradingViewChartInner({ symbol, allowSymbolChange = true }: Props) {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
function getTheme() {
return document.documentElement.classList.contains("dark")
? "dark"
: "light";
}
function createWidget() {
if (!container) return;
container.innerHTML = "";
const script = document.createElement("script");
script.src =
"https://s3.tradingview.com/external-embedding/embed-widget-advanced-chart.js";
script.type = "text/javascript";
script.async = true;
const rect = container.getBoundingClientRect();
const h = Math.max(Math.round(rect.height), 600);
script.innerHTML = JSON.stringify({
width: Math.round(rect.width),
height: h,
symbol,
interval: "D",
timezone: "Etc/UTC",
theme: getTheme(),
style:createWidget function · typescript · L23-L52 (30 LOC)src/components/assets/tradingview-chart.tsx
function createWidget() {
if (!container) return;
container.innerHTML = "";
const script = document.createElement("script");
script.src =
"https://s3.tradingview.com/external-embedding/embed-widget-advanced-chart.js";
script.type = "text/javascript";
script.async = true;
const rect = container.getBoundingClientRect();
const h = Math.max(Math.round(rect.height), 600);
script.innerHTML = JSON.stringify({
width: Math.round(rect.width),
height: h,
symbol,
interval: "D",
timezone: "Etc/UTC",
theme: getTheme(),
style: "1",
locale: "en",
allow_symbol_change: allowSymbolChange,
calendar: false,
studies: [],
hide_volume: true,
support_host: "https://www.tradingview.com",
});
container.appendChild(script);
}AllocationTooltip function · typescript · L50-L62 (13 LOC)src/components/dashboard/allocation-chart.tsx
function AllocationTooltip({ active, payload }: TooltipProps<number, string>) {
if (!active || !payload?.length) return null;
const d = payload[0];
return (
<div className="rounded-lg border bg-background p-2 shadow-sm text-sm space-y-0.5">
<p className="font-medium">{d.name}</p>
<p>{formatCurrency(d.value ?? 0)}</p>
<p className="text-muted-foreground">
{formatPercent((d.payload as { pct: number }).pct)}
</p>
</div>
);
}AllocationChart function · typescript · L64-L120 (57 LOC)src/components/dashboard/allocation-chart.tsx
export function AllocationChart({ byType, byAsset }: Props) {
const [mode, setMode] = useState<Mode>("type");
const data =
mode === "type"
? byType.map((d) => ({ name: d.type, value: d.value, pct: d.pct }))
: byAsset.map((d) => ({ name: d.symbol, value: d.value, pct: d.pct }));
return (
<Card className="h-full shadow-md hover:shadow-lg transition-shadow border-border/60">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
<CardTitle>Allocation</CardTitle>
<Tabs value={mode} onValueChange={(v) => setMode(v as Mode)}>
<TabsList>
<TabsTrigger value="type">By Type</TabsTrigger>
<TabsTrigger value="asset">By Asset</TabsTrigger>
</TabsList>
</Tabs>
</CardHeader>
<CardContent>
{data.length === 0 ? (
<EmptyState
icon={PieChartIcon}
title="No assets yet"
description="Add assets to your portfolios tDashboardNav function · typescript · L17-L62 (46 LOC)src/components/dashboard-nav.tsx
export function DashboardNav({ userName }: { userName: string }) {
const pathname = usePathname();
return (
<header className="border-b sticky top-0 z-40 bg-background">
<div className="container flex h-14 items-center gap-6">
<span className="font-semibold text-sm tracking-tight">FolioVault</span>
<nav className="flex items-center gap-1 flex-1">
{NAV_LINKS.map(({ href, label, icon: Icon }) => {
const active =
href === "/dashboard"
? pathname === "/dashboard"
: pathname.startsWith(href);
return (
<Link
key={href}
href={href}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm transition-colors ${
active
? "text-foreground bg-accent font-medium"
: "text-muted-foreground hover:text-foreground hover:bg-accent/50"
}`}
formatAxisDate function · typescript · L35-L41 (7 LOC)src/components/dashboard/performance-chart.tsx
function formatAxisDate(dateStr: string, period: Period) {
const d = new Date(dateStr + "T00:00:00");
if (period === "monthly") {
return d.toLocaleDateString("en-US", { month: "short", year: "2-digit" });
}
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}Repobility analyzer · published findings · https://repobility.com
ChartTooltip function · typescript · L43-L51 (9 LOC)src/components/dashboard/performance-chart.tsx
function ChartTooltip({ active, payload, label }: TooltipProps<number, string>) {
if (!active || !payload?.length) return null;
return (
<div className="rounded-lg border bg-background p-2 shadow-sm text-sm">
<p className="text-muted-foreground mb-1">{label}</p>
<p className="font-semibold">{formatCurrency(payload[0].value ?? 0)}</p>
</div>
);
}PerformanceChart function · typescript · L53-L117 (65 LOC)src/components/dashboard/performance-chart.tsx
export function PerformanceChart({ performance }: Props) {
const [period, setPeriod] = useState<Period>("daily");
const data = performance[period];
// Thin out x-axis ticks so they don't overlap
const tickCount = Math.min(data.length, 8);
const step = Math.max(1, Math.floor(data.length / tickCount));
const ticks = data
.filter((_, i) => i % step === 0 || i === data.length - 1)
.map((d) => d.date);
return (
<Card className="h-full shadow-md hover:shadow-lg transition-shadow border-border/60">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
<CardTitle>Portfolio Value</CardTitle>
<Tabs value={period} onValueChange={(v) => setPeriod(v as Period)}>
<TabsList>
<TabsTrigger value="daily">Daily</TabsTrigger>
<TabsTrigger value="weekly">Weekly</TabsTrigger>
<TabsTrigger value="monthly">Monthly</TabsTrigger>
</TabsList>
</Tabs>
</CardHeader>
SummaryCards function · typescript · L17-L59 (43 LOC)src/components/dashboard/summary-cards.tsx
export function SummaryCards({
totalValue,
totalCost,
totalGainLoss,
totalGainLossPct,
currency = "USD",
}: Props) {
const positive = totalGainLoss >= 0;
const gainColor = positive ? "text-positive" : "text-negative";
const gainBg = positive ? "bg-positive/10" : "bg-negative/10";
const GainIcon = positive ? TrendingUp : TrendingDown;
return (
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4">
<MetricCard
title="Portfolio Value"
value={formatCurrency(totalValue, currency)}
icon={DollarSign}
/>
<MetricCard
title="Total Invested"
value={formatCurrency(totalCost, currency)}
icon={BarChart2}
/>
<MetricCard
title="Profit / Loss"
value={(positive ? "+" : "−") + formatCurrency(Math.abs(totalGainLoss), currency)}
valueClassName={gainColor}
icon={GainIcon}
iconClassName={gainColor}
iconBgClassName={gainBg}
/>
<Metri