Function bodies 201 total
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 dispAssetTooltip 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 dAll 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-foregrohandleClose 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 classCitation: 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("");
setConfihandleDelete 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: cuhandleSaveCurrency 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>
<CardTitlehandleSave 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 ?? "FaihandleSubmit 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);
setActiveIndselectResult 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");
}
}
}