← back to idaturgutinal__portfolio-tracker

Function bodies 201 total

All specs Real LLM only Function bodies
MetricCard function · typescript · L61-L91 (31 LOC)
src/components/dashboard/summary-cards.tsx
function MetricCard({
  title,
  value,
  valueClassName,
  icon: Icon,
  iconClassName,
  iconBgClassName,
}: {
  title: string;
  value: string;
  valueClassName?: string;
  icon: LucideIcon;
  iconClassName?: string;
  iconBgClassName?: string;
}) {
  return (
    <Card className="shadow-md hover:shadow-lg transition-shadow border-border/60">
      <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
        <CardTitle className="text-sm font-medium text-muted-foreground">
          {title}
        </CardTitle>
        <div className={cn("flex h-10 w-10 items-center justify-center rounded-xl", iconBgClassName ?? "bg-primary/10")}>
          <Icon className={cn("h-5 w-5", iconClassName ?? "text-primary")} />
        </div>
      </CardHeader>
      <CardContent>
        <p className={`text-2xl font-bold ${valueClassName ?? ""}`}>{value}</p>
      </CardContent>
    </Card>
  );
}
TopMovers function · typescript · L16-L30 (15 LOC)
src/components/dashboard/top-movers.tsx
export function TopMovers({ topGainers, topLosers, pricesStale, currency = "USD" }: Props) {
  return (
    <div className="space-y-2">
      {pricesStale && (
        <p className="text-xs text-muted-foreground">
          ⚠ Some prices could not be fetched — showing last cached or cost-basis values.
        </p>
      )}
      <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
        <MoverCard title="Top Gainers" assets={topGainers} variant="positive" currency={currency} />
        <MoverCard title="Top Losers" assets={topLosers} variant="negative" currency={currency} />
      </div>
    </div>
  );
}
MoverCard function · typescript · L32-L94 (63 LOC)
src/components/dashboard/top-movers.tsx
function MoverCard({
  title,
  assets,
  variant,
  currency = "USD",
}: {
  title: string;
  assets: AssetMetric[];
  variant: "positive" | "negative";
  currency?: string;
}) {
  return (
    <Card className="shadow-md hover:shadow-lg transition-shadow border-border/60">
      <CardHeader className="pb-3">
        <div className="flex items-center gap-2.5">
          <div className={`flex h-10 w-10 items-center justify-center rounded-xl ${variant === "positive" ? "bg-positive/10" : "bg-negative/10"}`}>
            {variant === "positive" ? (
              <TrendingUp className="h-5 w-5 text-positive" />
            ) : (
              <TrendingDown className="h-5 w-5 text-negative" />
            )}
          </div>
          <CardTitle className="text-base">{title}</CardTitle>
        </div>
      </CardHeader>
      <CardContent>
        {assets.length === 0 ? (
          <div className="flex items-center gap-2 py-2 text-sm text-muted-foreground">
            <span>No data to disp
AssetTooltip function · typescript · L34-L45 (12 LOC)
src/components/dividends/dividend-by-asset.tsx
function AssetTooltip({ active, payload, currency }: TooltipProps<number, string> & { currency: string }) {
  if (!active || !payload?.length) return null;
  const d = payload[0].payload as { name: string; value: number; pct: number; count: number };
  return (
    <div className="rounded-lg border bg-background p-2 shadow-sm text-sm space-y-0.5">
      <p className="font-semibold font-mono">{d.name}</p>
      <p className="text-positive">{formatCurrency(d.value, currency)}</p>
      <p className="text-muted-foreground">{formatPercent(d.pct, false)}</p>
      <p className="text-muted-foreground text-xs">{d.count} payment{d.count !== 1 ? "s" : ""}</p>
    </div>
  );
}
DividendByAssetChart function · typescript · L47-L106 (60 LOC)
src/components/dividends/dividend-by-asset.tsx
export function DividendByAssetChart({ byAsset, currency = "USD" }: Props) {
  if (byAsset.length === 0) {
    return (
      <Card className="h-full shadow-md hover:shadow-lg transition-shadow border-border/60">
        <CardHeader>
          <CardTitle>Dividends by Asset</CardTitle>
        </CardHeader>
        <CardContent>
          <EmptyState
            icon={PieChartIcon}
            title="No dividends yet"
            description="Record dividend transactions to see breakdown by asset."
          />
        </CardContent>
      </Card>
    );
  }

  const data = byAsset.map((d) => ({
    name: d.symbol,
    value: d.total,
    pct: d.pct,
    count: d.count,
  }));

  return (
    <Card className="h-full shadow-md hover:shadow-lg transition-shadow border-border/60">
      <CardHeader>
        <CardTitle>Dividends by Asset</CardTitle>
      </CardHeader>
      <CardContent>
        <ResponsiveContainer width="100%" height={300}>
          <PieChart>
            <Pie
         
ChartTooltip function · typescript · L24-L32 (9 LOC)
src/components/dividends/dividend-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 space-y-0.5">
      <p className="font-medium">{label}</p>
      <p className="text-positive">{formatCurrency(payload[0].value as number)}</p>
    </div>
  );
}
DividendChart function · typescript · L34-L87 (54 LOC)
src/components/dividends/dividend-chart.tsx
export function DividendChart({ monthly, currency = "USD" }: Props) {
  if (monthly.length === 0) {
    return (
      <Card className="shadow-md hover:shadow-lg transition-shadow border-border/60">
        <CardHeader>
          <CardTitle>Monthly Dividends</CardTitle>
        </CardHeader>
        <CardContent>
          <EmptyState
            icon={BarChart2}
            title="No dividends yet"
            description="Record dividend transactions to see your monthly dividend income."
          />
        </CardContent>
      </Card>
    );
  }

  // Show last 12 months max
  const data = monthly.slice(-12).map((d) => ({
    month: d.month,
    label: formatMonthLabel(d.month),
    total: d.total,
  }));

  return (
    <Card className="shadow-md hover:shadow-lg transition-shadow border-border/60">
      <CardHeader>
        <CardTitle>Monthly Dividends</CardTitle>
      </CardHeader>
      <CardContent>
        <ResponsiveContainer width="100%" height={300}>
          <BarChart d
All rows scored by the Repobility analyzer (https://repobility.com)
DividendHistoryTable function · typescript · L26-L118 (93 LOC)
src/components/dividends/dividend-history-table.tsx
export function DividendHistoryTable({ transactions, currency = "USD" }: Props) {
  const [page, setPage] = useState(0);

  const totalPages = Math.max(1, Math.ceil(transactions.length / PAGE_SIZE));
  const paged = useMemo(
    () => transactions.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE),
    [transactions, page]
  );

  if (transactions.length === 0) {
    return (
      <Card className="shadow-md hover:shadow-lg transition-shadow border-border/60">
        <CardHeader>
          <CardTitle>Dividend History</CardTitle>
        </CardHeader>
        <CardContent>
          <EmptyState
            icon={DollarSign}
            title="No dividends recorded"
            description="Add dividend transactions to your assets to track your income."
          />
        </CardContent>
      </Card>
    );
  }

  return (
    <Card className="shadow-md hover:shadow-lg transition-shadow border-border/60">
      <CardHeader className="flex flex-row items-center justify-between space-y-0">
DividendSummaryCards function · typescript · L16-L55 (40 LOC)
src/components/dividends/dividend-summary-cards.tsx
export function DividendSummaryCards({
  totalDividends,
  monthlyAverage,
  last12Months,
  totalCount,
  currency = "USD",
}: Props) {
  return (
    <div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4">
      <MetricCard
        title="Total Dividends"
        value={formatCurrency(totalDividends, currency)}
        icon={DollarSign}
        iconBg="bg-positive/10"
        iconColor="text-positive"
      />
      <MetricCard
        title="Last 12 Months"
        value={formatCurrency(last12Months, currency)}
        icon={TrendingUp}
        iconBg="bg-blue-500/10"
        iconColor="text-blue-500"
      />
      <MetricCard
        title="Monthly Average"
        value={formatCurrency(monthlyAverage, currency)}
        icon={BarChart2}
        iconBg="bg-amber-500/10"
        iconColor="text-amber-500"
      />
      <MetricCard
        title="Total Payments"
        value={totalCount.toString()}
        icon={ArrowUpDown}
        iconBg="bg-purple-500/10"
       
MetricCard function · typescript · L57-L85 (29 LOC)
src/components/dividends/dividend-summary-cards.tsx
function MetricCard({
  title,
  value,
  icon: Icon,
  iconBg,
  iconColor,
}: {
  title: string;
  value: string;
  icon: React.ComponentType<{ className?: string }>;
  iconBg: string;
  iconColor: string;
}) {
  return (
    <Card className="shadow-md hover:shadow-lg transition-shadow border-border/60">
      <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
        <CardTitle className="text-sm font-medium text-muted-foreground">
          {title}
        </CardTitle>
        <div className={cn("flex h-10 w-10 items-center justify-center rounded-xl", iconBg)}>
          <Icon className={cn("h-5 w-5", iconColor)} />
        </div>
      </CardHeader>
      <CardContent>
        <p className="text-2xl font-bold">{value}</p>
      </CardContent>
    </Card>
  );
}
EmptyState function · typescript · L10-L23 (14 LOC)
src/components/empty-state.tsx
export function EmptyState({ icon: Icon, title, description, action }: EmptyStateProps) {
  return (
    <div className="flex flex-col items-center justify-center gap-4 py-16 text-center">
      <div className="rounded-xl bg-primary/10 p-5 shadow-sm">
        <Icon className="h-10 w-10 text-primary" />
      </div>
      <div className="space-y-1">
        <p className="font-semibold">{title}</p>
        <p className="text-sm text-muted-foreground max-w-sm">{description}</p>
      </div>
      {action && <div>{action}</div>}
    </div>
  );
}
FolioVaultLogo function · typescript · L8-L32 (25 LOC)
src/components/folio-vault-logo.tsx
export function FolioVaultLogo({ size = 24, className }: FolioVaultLogoProps) {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      className={cn("shrink-0", className)}
    >
      {/* Shield outline */}
      <path
        d="M12 2.5L20.5 6.5V13C20.5 17.5 12 21.5 12 21.5C12 21.5 3.5 17.5 3.5 13V6.5L12 2.5Z"
        stroke="currentColor"
        strokeWidth="1.5"
        strokeLinejoin="round"
        strokeLinecap="round"
      />
      {/* Ascending bar chart */}
      <rect x="7.5" y="14" width="2.5" height="2" rx="0.4" fill="currentColor" />
      <rect x="10.75" y="11.5" width="2.5" height="4.5" rx="0.4" fill="currentColor" />
      <rect x="14" y="9" width="2.5" height="7" rx="0.4" fill="currentColor" />
    </svg>
  );
}
OnboardingDialog function · typescript · L22-L95 (74 LOC)
src/components/onboarding/onboarding-dialog.tsx
export function OnboardingDialog({ userId }: Props) {
  const [open, setOpen] = useState(false);

  useEffect(() => {
    try {
      if (!localStorage.getItem(storageKey(userId))) {
        setOpen(true);
      }
    } catch {
      // localStorage unavailable (SSR guard, incognito strict mode)
    }
  }, [userId]);

  function handleClose() {
    try {
      localStorage.setItem(storageKey(userId), "1");
    } catch {}
    setOpen(false);
  }

  return (
    <Dialog open={open} onOpenChange={(v) => { if (!v) handleClose(); }}>
      <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
        <DialogHeader className="pb-2">
          <div className="flex items-center gap-2">
            <span className="rounded-lg bg-primary/10 p-2">
              <Rocket className="h-5 w-5 text-primary" />
            </span>
            <div>
              <DialogTitle className="text-xl">Welcome to Portfolio Tracker</DialogTitle>
              <p className="text-sm text-muted-foregro
handleClose function · typescript · L35-L40 (6 LOC)
src/components/onboarding/onboarding-dialog.tsx
  function handleClose() {
    try {
      localStorage.setItem(storageKey(userId), "1");
    } catch {}
    setOpen(false);
  }
UserGuidePanel function · typescript · L11-L94 (84 LOC)
src/components/onboarding/user-guide-panel.tsx
export function UserGuidePanel() {
  // Start collapsed to avoid a flash; useEffect will set the real state
  const [collapsed, setCollapsed] = useState(true);
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    try {
      setCollapsed(localStorage.getItem(COLLAPSE_KEY) === "1");
    } catch {}
    setMounted(true);
  }, []);

  function toggle() {
    const next = !collapsed;
    setCollapsed(next);
    try {
      if (next) {
        localStorage.setItem(COLLAPSE_KEY, "1");
      } else {
        localStorage.removeItem(COLLAPSE_KEY);
      }
    } catch {}
  }

  // Avoid layout shift on first render
  if (!mounted) return null;

  return (
    <Card>
      <CardHeader className="py-4 px-6">
        <div className="flex items-center justify-between">
          <div className="flex items-center gap-2">
            <BookOpen className="h-4 w-4 text-muted-foreground" />
            <span className="font-semibold text-sm">Quick Guide</span>
            <span class
Citation: Repobility (2026). State of AI-Generated Code. https://repobility.com/research/
toggle function · typescript · L23-L33 (11 LOC)
src/components/onboarding/user-guide-panel.tsx
  function toggle() {
    const next = !collapsed;
    setCollapsed(next);
    try {
      if (next) {
        localStorage.setItem(COLLAPSE_KEY, "1");
      } else {
        localStorage.removeItem(COLLAPSE_KEY);
      }
    } catch {}
  }
PageHeader function · typescript · L9-L21 (13 LOC)
src/components/page-header.tsx
export function PageHeader({ icon: Icon, title, description }: PageHeaderProps) {
  return (
    <div className="flex items-center gap-4">
      <div className="flex items-center justify-center h-10 w-10 rounded-lg bg-primary/10 shrink-0">
        <Icon className="h-5 w-5 text-primary" />
      </div>
      <div>
        <h1 className="text-3xl font-bold tracking-tight">{title}</h1>
        <p className="text-muted-foreground mt-0.5">{description}</p>
      </div>
    </div>
  );
}
Providers function · typescript · L6-L13 (8 LOC)
src/components/providers.tsx
export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <SessionProvider>
      {children}
      <Toaster />
    </SessionProvider>
  );
}
DangerZoneTab function · typescript · L18-L133 (116 LOC)
src/components/settings/danger-zone-tab.tsx
export function DangerZoneTab({ hasPassword }: { hasPassword: boolean }) {
  const [open, setOpen] = useState(false);
  const [password, setPassword] = useState("");
  const [confirmation, setConfirmation] = useState("");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  async function handleDelete() {
    setError(null);
    setLoading(true);
    try {
      const res = await fetch("/api/user/delete", {
        method: "DELETE",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(
          hasPassword ? { password } : { confirmation }
        ),
      });
      const data = await res.json();
      if (!res.ok) {
        setError(data.error ?? "Failed to delete account");
        return;
      }
      await signOut({ callbackUrl: "/login" });
    } finally {
      setLoading(false);
    }
  }

  function handleOpenChange(next: boolean) {
    if (!next) {
      setPassword("");
      setConfi
handleDelete function · typescript · L25-L45 (21 LOC)
src/components/settings/danger-zone-tab.tsx
  async function handleDelete() {
    setError(null);
    setLoading(true);
    try {
      const res = await fetch("/api/user/delete", {
        method: "DELETE",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(
          hasPassword ? { password } : { confirmation }
        ),
      });
      const data = await res.json();
      if (!res.ok) {
        setError(data.error ?? "Failed to delete account");
        return;
      }
      await signOut({ callbackUrl: "/login" });
    } finally {
      setLoading(false);
    }
  }
handleOpenChange function · typescript · L47-L54 (8 LOC)
src/components/settings/danger-zone-tab.tsx
  function handleOpenChange(next: boolean) {
    if (!next) {
      setPassword("");
      setConfirmation("");
      setError(null);
    }
    setOpen(next);
  }
triggerDownload function · typescript · L9-L16 (8 LOC)
src/components/settings/export-tab.tsx
function triggerDownload(format: ExportFormat) {
  const a = document.createElement("a");
  a.href = `/api/user/export?format=${format}`;
  a.download = "";
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
}
ExportTab function · typescript · L37-L66 (30 LOC)
src/components/settings/export-tab.tsx
export function ExportTab() {
  return (
    <Card>
      <CardHeader>
        <CardTitle>Export Data</CardTitle>
      </CardHeader>
      <CardContent className="space-y-4">
        {EXPORTS.map(({ format, label, description }) => (
          <div
            key={format}
            className="flex items-center justify-between gap-4 py-3 border-b last:border-0"
          >
            <div>
              <p className="font-medium text-sm">{label}</p>
              <p className="text-xs text-muted-foreground">{description}</p>
            </div>
            <Button
              variant="outline"
              size="sm"
              onClick={() => triggerDownload(format)}
            >
              <Download className="h-3.5 w-3.5 mr-1.5" />
              Download
            </Button>
          </div>
        ))}
      </CardContent>
    </Card>
  );
}
All rows above produced by Repobility · https://repobility.com
PreferencesTab function · typescript · L36-L138 (103 LOC)
src/components/settings/preferences-tab.tsx
export function PreferencesTab({
  defaultCurrency,
}: {
  defaultCurrency: string;
}) {
  const { update } = useSession();
  const router = useRouter();
  const { theme, setTheme } = useTheme();

  const [currency, setCurrency] = useState(defaultCurrency);
  const [currencyLoading, setCurrencyLoading] = useState(false);
  const [currencyError, setCurrencyError] = useState<string | null>(null);
  const [currencySuccess, setCurrencySuccess] = useState(false);

  async function handleSaveCurrency() {
    setCurrencyError(null);
    setCurrencySuccess(false);
    setCurrencyLoading(true);
    try {
      const res = await fetch("/api/user/profile", {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ defaultCurrency: currency }),
      });
      const data = await res.json();
      if (!res.ok) {
        setCurrencyError(data.error ?? "Failed to update currency");
        return;
      }
      await update({ defaultCurrency: cu
handleSaveCurrency function · typescript · L50-L71 (22 LOC)
src/components/settings/preferences-tab.tsx
  async function handleSaveCurrency() {
    setCurrencyError(null);
    setCurrencySuccess(false);
    setCurrencyLoading(true);
    try {
      const res = await fetch("/api/user/profile", {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ defaultCurrency: currency }),
      });
      const data = await res.json();
      if (!res.ok) {
        setCurrencyError(data.error ?? "Failed to update currency");
        return;
      }
      await update({ defaultCurrency: currency });
      setCurrencySuccess(true);
      router.refresh();
    } finally {
      setCurrencyLoading(false);
    }
  }
ProfileTab function · typescript · L12-L78 (67 LOC)
src/components/settings/profile-tab.tsx
export function ProfileTab({ profile }: { profile: UserProfile }) {
  const { update } = useSession();
  const [name, setName] = useState(profile.name);
  const [email, setEmail] = useState(profile.email);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [success, setSuccess] = useState(false);

  async function handleSave() {
    setError(null);
    setSuccess(false);
    setLoading(true);
    try {
      const res = await fetch("/api/user/profile", {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name, email }),
      });
      const data = await res.json();
      if (!res.ok) {
        setError(data.error ?? "Failed to update profile");
        return;
      }
      await update({ name: data.name, email: data.email });
      setSuccess(true);
    } finally {
      setLoading(false);
    }
  }

  return (
    <Card>
      <CardHeader>
        <CardTitle
handleSave function · typescript · L20-L40 (21 LOC)
src/components/settings/profile-tab.tsx
  async function handleSave() {
    setError(null);
    setSuccess(false);
    setLoading(true);
    try {
      const res = await fetch("/api/user/profile", {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name, email }),
      });
      const data = await res.json();
      if (!res.ok) {
        setError(data.error ?? "Failed to update profile");
        return;
      }
      await update({ name: data.name, email: data.email });
      setSuccess(true);
    } finally {
      setLoading(false);
    }
  }
SecurityTab function · typescript · L10-L152 (143 LOC)
src/components/settings/security-tab.tsx
export function SecurityTab({ hasPassword }: { hasPassword: boolean }) {
  const [currentPassword, setCurrentPassword] = useState("");
  const [newPassword, setNewPassword] = useState("");
  const [confirmPassword, setConfirmPassword] = useState("");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [success, setSuccess] = useState(false);
  const [passwordSet, setPasswordSet] = useState(hasPassword);

  async function handleUpdate() {
    setError(null);
    setSuccess(false);

    if (newPassword !== confirmPassword) {
      setError("New passwords do not match");
      return;
    }
    if (newPassword.length < 8) {
      setError("New password must be at least 8 characters");
      return;
    }

    setLoading(true);
    try {
      const res = await fetch("/api/user/password", {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(
          passwordSet
    
handleUpdate function · typescript · L19-L56 (38 LOC)
src/components/settings/security-tab.tsx
  async function handleUpdate() {
    setError(null);
    setSuccess(false);

    if (newPassword !== confirmPassword) {
      setError("New passwords do not match");
      return;
    }
    if (newPassword.length < 8) {
      setError("New password must be at least 8 characters");
      return;
    }

    setLoading(true);
    try {
      const res = await fetch("/api/user/password", {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(
          passwordSet
            ? { currentPassword, newPassword }
            : { newPassword }
        ),
      });
      const data = await res.json();
      if (!res.ok) {
        setError(data.error ?? "Failed to update password");
        return;
      }
      setSuccess(true);
      setCurrentPassword("");
      setNewPassword("");
      setConfirmPassword("");
      if (!passwordSet) setPasswordSet(true);
    } finally {
      setLoading(false);
    }
  }
SettingsShell function · typescript · L11-L43 (33 LOC)
src/components/settings/settings-shell.tsx
export function SettingsShell({ profile }: { profile: UserProfile }) {
  return (
    <Tabs defaultValue="profile">
      <TabsList className="mb-6">
        <TabsTrigger value="profile">Profile</TabsTrigger>
        <TabsTrigger value="security">Security</TabsTrigger>
        <TabsTrigger value="preferences">Preferences</TabsTrigger>
        <TabsTrigger value="export">Export</TabsTrigger>
        <TabsTrigger value="danger">Danger Zone</TabsTrigger>
      </TabsList>

      <TabsContent value="profile">
        <ProfileTab profile={profile} />
      </TabsContent>

      <TabsContent value="security">
        <SecurityTab hasPassword={profile.hasPassword} />
      </TabsContent>

      <TabsContent value="preferences">
        <PreferencesTab defaultCurrency={profile.defaultCurrency} />
      </TabsContent>

      <TabsContent value="export">
        <ExportTab />
      </TabsContent>

      <TabsContent value="danger">
        <DangerZoneTab hasPassword={profile.hasPassword} />
    
SidebarNav function · typescript · L46-L163 (118 LOC)
src/components/sidebar-nav.tsx
export function SidebarNav({ userName, userEmail }: Props) {
  const pathname = usePathname();
  const [open, setOpen] = useState(false);

  // Close drawer on route change
  useEffect(() => {
    setOpen(false);
  }, [pathname]);

  function isActive(href: string) {
    return href === "/dashboard"
      ? pathname === "/dashboard"
      : pathname.startsWith(href);
  }

  const navLinkClass = (href: string) =>
    cn(
      "flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors transition-shadow",
      isActive(href)
        ? "bg-accent text-accent-foreground shadow-sm border border-border/50"
        : "text-muted-foreground hover:text-foreground hover:bg-accent/50"
    );

  const SidebarContent = () => (
    <aside className="flex flex-col h-full">
      {/* Logo */}
      <div className="flex items-center gap-2 h-14 px-4 border-b shrink-0">
        <div className="flex items-center gap-2.5 rounded-full bg-muted/80 px-3.5 py-2 shadow-sm">
          <
Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
SupportForm function · typescript · L10-L96 (87 LOC)
src/components/support/support-form.tsx
export function SupportForm() {
  const [subject, setSubject] = useState("");
  const [message, setMessage] = useState("");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [success, setSuccess] = useState(false);

  async function handleSubmit() {
    setError(null);
    setSuccess(false);

    if (!subject.trim()) {
      setError("Please enter a subject.");
      return;
    }
    if (!message.trim()) {
      setError("Please enter a message.");
      return;
    }
    if (message.length > 2000) {
      setError("Message cannot exceed 2000 characters.");
      return;
    }

    setLoading(true);
    try {
      const res = await fetch("/api/support", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ subject: subject.trim(), message: message.trim() }),
      });
      const data = await res.json();
      if (!res.ok) {
        setError(data.error ?? "Fai
handleSubmit function · typescript · L17-L54 (38 LOC)
src/components/support/support-form.tsx
  async function handleSubmit() {
    setError(null);
    setSuccess(false);

    if (!subject.trim()) {
      setError("Please enter a subject.");
      return;
    }
    if (!message.trim()) {
      setError("Please enter a message.");
      return;
    }
    if (message.length > 2000) {
      setError("Message cannot exceed 2000 characters.");
      return;
    }

    setLoading(true);
    try {
      const res = await fetch("/api/support", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ subject: subject.trim(), message: message.trim() }),
      });
      const data = await res.json();
      if (!res.ok) {
        setError(data.error ?? "Failed to send message.");
        return;
      }
      setSuccess(true);
      setSubject("");
      setMessage("");
    } catch {
      setError("Something went wrong. Please try again.");
    } finally {
      setLoading(false);
    }
  }
SymbolSearch function · typescript · L10-L143 (134 LOC)
src/components/symbol-search.tsx
export function SymbolSearch() {
  const router = useRouter();
  const [query, setQuery] = useState("");
  const [results, setResults] = useState<SymbolSearchResult[]>([]);
  const [open, setOpen] = useState(false);
  const [activeIndex, setActiveIndex] = useState(-1);
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const abortRef = useRef<AbortController | null>(null);
  const containerRef = useRef<HTMLDivElement>(null);

  // Close dropdown on outside click
  useEffect(() => {
    function handleClickOutside(e: MouseEvent) {
      if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
        setOpen(false);
      }
    }
    document.addEventListener("mousedown", handleClickOutside);
    return () => document.removeEventListener("mousedown", handleClickOutside);
  }, []);

  const handleChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      const value = e.target.value;
      setQuery(value);
      setActiveInd
selectResult function · typescript · L67-L73 (7 LOC)
src/components/symbol-search.tsx
  function selectResult(r: SymbolSearchResult) {
    const type = r.suggestedType || "STOCK";
    router.push(`/dashboard/chart/${encodeURIComponent(r.symbol)}?type=${type}`);
    setQuery("");
    setResults([]);
    setOpen(false);
  }
handleKeyDown function · typescript · L75-L98 (24 LOC)
src/components/symbol-search.tsx
  function handleKeyDown(e: React.KeyboardEvent) {
    if (!open || results.length === 0) return;

    switch (e.key) {
      case "ArrowDown":
        e.preventDefault();
        setActiveIndex((prev) => (prev < results.length - 1 ? prev + 1 : 0));
        break;
      case "ArrowUp":
        e.preventDefault();
        setActiveIndex((prev) => (prev > 0 ? prev - 1 : results.length - 1));
        break;
      case "Enter":
        e.preventDefault();
        if (activeIndex >= 0 && activeIndex < results.length) {
          selectResult(results[activeIndex]);
        }
        break;
      case "Escape":
        e.preventDefault();
        setOpen(false);
        break;
    }
  }
defaultForm function · typescript · L42-L52 (11 LOC)
src/components/transactions/add-transaction-dialog.tsx
function defaultForm(): FormState {
  return {
    assetId: "",
    type: "BUY",
    quantity: "",
    pricePerUnit: "",
    fees: "0",
    date: new Date().toISOString().split("T")[0],
    notes: "",
  };
}
handleSubmit function · typescript · L86-L120 (35 LOC)
src/components/transactions/add-transaction-dialog.tsx
  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setError(null);

    if (!form.assetId) return setError("Please select an asset.");
    if (isNaN(qty) || qty <= 0) return setError("Quantity must be a positive number.");
    if (isNaN(price) || price <= 0) return setError("Price per unit must be positive.");
    if (!isNaN(fees) && fees < 0) return setError("Fees cannot be negative.");
    if (!form.date) return setError("Date is required.");

    setLoading(true);
    const res = await fetch("/api/transactions", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        assetId: form.assetId,
        type: form.type,
        quantity: qty,
        pricePerUnit: price,
        fees: isNaN(fees) ? 0 : fees,
        date: new Date(form.date + "T12:00:00").toISOString(),
        notes: form.notes.trim() || undefined,
      }),
    });
    setLoading(false);

    if (!res.ok) {
      const body = await 
exportToCSV function · typescript · L37-L75 (39 LOC)
src/components/transactions/transactions-table.tsx
function exportToCSV(transactions: EnrichedTransaction[]) {
  const headers = [
    "Date",
    "Portfolio",
    "Symbol",
    "Name",
    "Type",
    "Quantity",
    "Price/Unit",
    "Fees",
    "Total",
    "Notes",
  ];

  const esc = (v: string | number | null | undefined) =>
    `"${String(v ?? "").replace(/"/g, '""')}"`;

  const rows = transactions.map((t) => [
    formatDate(t.date),
    t.portfolioName,
    t.assetSymbol,
    t.assetName,
    t.type,
    t.quantity,
    t.pricePerUnit,
    t.fees,
    t.total,
    t.notes ?? "",
  ]);

  const csv = [headers, ...rows].map((row) => row.map(esc).join(",")).join("\n");
  const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = `transactions-${new Date().toISOString().split("T")[0]}.csv`;
  a.click();
  URL.revokeObjectURL(url);
}
All rows scored by the Repobility analyzer (https://repobility.com)
clearFilters function · typescript · L133-L138 (6 LOC)
src/components/transactions/transactions-table.tsx
  function clearFilters() {
    setDateFrom("");
    setDateTo("");
    setAssetFilter("all");
    setTypeFilter("all");
  }
ConfirmDialog function · typescript · L25-L62 (38 LOC)
src/components/ui/confirm-dialog.tsx
export function ConfirmDialog({
  open,
  onOpenChange,
  title,
  description,
  confirmLabel = "Confirm",
  loadingLabel = "Processing…",
  loading = false,
  variant = "destructive",
  onConfirm,
}: ConfirmDialogProps) {
  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className="max-w-sm">
        <DialogHeader>
          <DialogTitle>{title}</DialogTitle>
          <DialogDescription>{description}</DialogDescription>
        </DialogHeader>
        <DialogFooter>
          <Button
            variant="outline"
            onClick={() => onOpenChange(false)}
            disabled={loading}
          >
            Cancel
          </Button>
          <Button
            variant={variant}
            onClick={onConfirm}
            disabled={loading}
          >
            {loading ? loadingLabel : confirmLabel}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}
PaginationControls function · typescript · L12-L46 (35 LOC)
src/components/ui/pagination.tsx
export function PaginationControls({
  page,
  totalPages,
  onPageChange,
}: PaginationControlsProps) {
  if (totalPages <= 1) return null;

  return (
    <div className="flex items-center justify-between">
      <p className="text-sm text-muted-foreground">
        Page {page} of {totalPages}
      </p>
      <div className="flex gap-2">
        <Button
          variant="outline"
          size="sm"
          onClick={() => onPageChange(Math.max(1, page - 1))}
          disabled={page <= 1}
        >
          <ChevronLeft className="h-4 w-4 mr-1" />
          Previous
        </Button>
        <Button
          variant="outline"
          size="sm"
          onClick={() => onPageChange(Math.min(totalPages, page + 1))}
          disabled={page >= totalPages}
        >
          Next
          <ChevronRight className="h-4 w-4 ml-1" />
        </Button>
      </div>
    </div>
  );
}
Skeleton function · typescript · L3-L10 (8 LOC)
src/components/ui/skeleton.tsx
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      className={cn("animate-pulse rounded-md bg-muted", className)}
      {...props}
    />
  );
}
Toaster function · typescript · L13-L32 (20 LOC)
src/components/ui/toaster.tsx
export function Toaster() {
  const { toasts, dismiss } = useToasts();

  return (
    <ToastProvider>
      {toasts.map((t) => (
        <Toast key={t.id} variant={t.variant} onOpenChange={(open) => { if (!open) dismiss(t.id); }}>
          <div className="flex-1 space-y-1">
            <ToastTitle>{t.title}</ToastTitle>
            {t.description && (
              <ToastDescription>{t.description}</ToastDescription>
            )}
          </div>
          <ToastClose />
        </Toast>
      ))}
      <ToastViewport />
    </ToastProvider>
  );
}
AddWatchlistDialog function · typescript · L41-L237 (197 LOC)
src/components/watchlist/add-watchlist-dialog.tsx
export function AddWatchlistDialog({ open, onOpenChange, onSuccess }: Props) {
  const [form, setForm] = useState<FormState>(defaultForm);
  const [results, setResults] = useState<SymbolSearchResult[]>([]);
  const [dropdownOpen, setDropdownOpen] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  useEffect(() => {
    if (!open) return;
    setForm(defaultForm());
    setError(null);
    setResults([]);
    setDropdownOpen(false);
  }, [open]);

  function set<K extends keyof FormState>(key: K, value: FormState[K]) {
    setForm((prev) => ({ ...prev, [key]: value }));
  }

  const handleSymbolChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      const value = e.target.value;
      set("symbol", value);
      if (timerRef.current) clearTimeout(timerRef.current);
      if (!value.trim()) {
        setResults([]);
   
handleSelectResult function · typescript · L85-L94 (10 LOC)
src/components/watchlist/add-watchlist-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);
  }
handleSubmit function · typescript · L96-L124 (29 LOC)
src/components/watchlist/add-watchlist-dialog.tsx
  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setError(null);

    if (!form.symbol.trim()) return setError("Symbol is required.");
    if (!form.name.trim()) return setError("Name is required.");

    setLoading(true);
    const res = await fetch("/api/watchlist", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        symbol: form.symbol.trim().toUpperCase(),
        name: form.name.trim(),
        assetType: form.assetType,
        notes: form.notes.trim() || undefined,
      }),
    });
    setLoading(false);

    if (!res.ok) {
      const body = await res.json().catch(() => ({}));
      setError((body as { error?: string }).error ?? "Something went wrong.");
      return;
    }

    onSuccess();
    onOpenChange(false);
  }
Citation: Repobility (2026). State of AI-Generated Code. https://repobility.com/research/
WatchlistTable function · typescript · L26-L186 (161 LOC)
src/components/watchlist/watchlist-table.tsx
export function WatchlistTable({ initialItems }: Props) {
  const router = useRouter();
  const [isPending, startTransition] = useTransition();
  const [addOpen, setAddOpen] = useState(false);
  const [items, setItems] = useState(initialItems);

  function refresh() {
    startTransition(() => router.refresh());
  }

  async function handleRemove(id: string) {
    setItems((prev) => prev.filter((i) => i.id !== id));
    await fetch(`/api/watchlist/${id}`, { method: "DELETE" });
    refresh();
  }

  return (
    <div className="space-y-4">
      <div className="flex items-center justify-between">
        <p className="text-sm text-muted-foreground">
          {items.length} symbol{items.length !== 1 ? "s" : ""}
        </p>
        <div className="flex gap-2">
          <Button
            variant="outline"
            size="icon"
            onClick={refresh}
            disabled={isPending}
            title="Refresh prices"
          >
            <RefreshCw
              className=
usePortfolio function · typescript · L11-L33 (23 LOC)
src/hooks/use-portfolio.ts
export function usePortfolio(portfolioId: string | null) {
  const [data, setData] = useState<PortfolioSummary | null>(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    if (!portfolioId) return;

    setIsLoading(true);
    setError(null);

    fetch(`/api/portfolios/${portfolioId}`)
      .then((res) => {
        if (!res.ok) throw new Error(`Failed to load portfolio: ${res.status}`);
        return res.json() as Promise<PortfolioSummary>;
      })
      .then(setData)
      .catch((err: Error) => setError(err))
      .finally(() => setIsLoading(false));
  }, [portfolioId]);

  return { data, isLoading, error };
}
applyTheme function · typescript · L9-L23 (15 LOC)
src/hooks/use-theme.ts
function applyTheme(theme: Theme) {
  const root = document.documentElement;
  if (theme === "dark") {
    root.classList.add("dark");
  } else if (theme === "light") {
    root.classList.remove("dark");
  } else {
    // system
    if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
      root.classList.add("dark");
    } else {
      root.classList.remove("dark");
    }
  }
}
‹ prevpage 3 / 5next ›