← back to idaturgutinal__portfolio-tracker

Function bodies 201 total

All specs Real LLM only Function bodies
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} />
        </di
SettingsLoading 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-16
TransactionsPage 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">
      <PageHe
RootLayout 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
        vo
TermsPage 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 (&quot;Terms&quot;). 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} clas
Repobility 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();
    setErr
handleAssetChange 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&amp;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.targetPri
handleCreate 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">&mdash;</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.trim
handleSort 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 t
DashboardNav 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
‹ prevpage 2 / 5next ›