Function bodies 201 total
DELETE function · typescript · L5-L19 (15 LOC)src/app/api/alerts/[id]/route.ts
export async function DELETE(
_req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const userId = await getSessionUserId();
if (!userId) return unauthorizedResponse();
try {
const { id } = await params;
await deleteAlert(id, userId);
return new NextResponse(null, { status: 204 });
} catch {
return serverError();
}
}PATCH function · typescript · L21-L35 (15 LOC)src/app/api/alerts/[id]/route.ts
export async function PATCH(
_req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const userId = await getSessionUserId();
if (!userId) return unauthorizedResponse();
try {
const { id } = await params;
await reactivateAlert(id, userId);
return NextResponse.json({ success: true });
} catch {
return serverError();
}
}GET function · typescript · L6-L16 (11 LOC)src/app/api/alerts/route.ts
export async function GET() {
const userId = await getSessionUserId();
if (!userId) return unauthorizedResponse();
try {
const alerts = await getAlertsByUser(userId);
return NextResponse.json(alerts);
} catch {
return serverError();
}
}POST function · typescript · L18-L44 (27 LOC)src/app/api/alerts/route.ts
export async function POST(req: NextRequest) {
const userId = await getSessionUserId();
if (!userId) return unauthorizedResponse();
try {
const body = (await req.json()) as Partial<CreateAlertInput>;
const { assetId, symbol, condition, targetPrice } = body;
if (!assetId || !symbol || !condition || targetPrice == null) {
return badRequest("Missing required fields.");
}
if (condition !== "ABOVE" && condition !== "BELOW") {
return badRequest("Condition must be ABOVE or BELOW.");
}
if (typeof targetPrice !== "number" || !isFinite(targetPrice) || targetPrice <= 0) {
return badRequest("Target price must be a positive number.");
}
if (typeof symbol !== "string" || symbol.trim().length === 0 || symbol.length > 20) {
return badRequest("Invalid symbol.");
}
const alert = await createAlert(userId, body as CreateAlertInput);
return NextResponse.json(alert, { status: 201 });
} catch {
return serverError();
}
}PATCH function · typescript · L15-L80 (66 LOC)src/app/api/assets/[id]/route.ts
export async function PATCH(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const userId = await getSessionUserId();
if (!userId) return unauthorizedResponse();
try {
const { id } = await params;
const existing = await assertOwner(id, userId);
if (!existing) {
return notFound("Asset not found.");
}
const body = (await req.json()) as UpdateAssetInput;
// Validate optional fields that are present
if (body.assetType !== undefined && !VALID_ASSET_TYPES.has(body.assetType)) {
return badRequest("Invalid asset type.");
}
if (body.symbol !== undefined) {
const s = body.symbol.trim();
if (!s || s.length > 20) {
return badRequest("Symbol must be 1–20 characters.");
}
body.symbol = s.toUpperCase();
}
if (body.name !== undefined) {
const n = body.name.trim();
if (!n || n.length > 200) {
return badRequest("Name must be 1–200 characters.");
}
bodDELETE function · typescript · L82-L101 (20 LOC)src/app/api/assets/[id]/route.ts
export async function DELETE(
_req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const userId = await getSessionUserId();
if (!userId) return unauthorizedResponse();
try {
const { id } = await params;
const existing = await assertOwner(id, userId);
if (!existing) {
return notFound("Asset not found.");
}
await deleteAsset(id);
return new NextResponse(null, { status: 204 });
} catch {
return serverError();
}
}POST function · typescript · L9-L64 (56 LOC)src/app/api/assets/route.ts
export async function POST(req: NextRequest) {
const userId = await getSessionUserId();
if (!userId) return unauthorizedResponse();
try {
const body = (await req.json()) as Partial<CreateAssetInput>;
const { portfolioId, symbol, name, assetType, quantity, averageBuyPrice, currency, notes } =
body;
// Required field presence
if (!portfolioId || !symbol || !name || !assetType || quantity == null || averageBuyPrice == null || !currency) {
return badRequest("Missing required fields.");
}
// Type validation
if (!VALID_ASSET_TYPES.has(assetType)) {
return badRequest("Invalid asset type.");
}
if (typeof symbol !== "string" || symbol.trim().length === 0 || symbol.length > 20) {
return badRequest("Symbol must be 1–20 characters.");
}
if (typeof name !== "string" || name.trim().length === 0 || name.length > 200) {
return badRequest("Name must be 1–200 characters.");
}
if (typeof quantity !== "number" || !iRepobility (the analyzer behind this table) · https://repobility.com
POST function · typescript · L8-L61 (54 LOC)src/app/api/auth/forgot-password/route.ts
export async function POST(req: NextRequest) {
const ip = getClientIp(req);
const rl = rateLimit(`forgot-password:${ip}`, 5, 15 * 60 * 1000);
if (!rl.allowed) {
return NextResponse.json(
{ error: "Too many requests. Please try again later." },
{ status: 429, headers: { "Retry-After": String(Math.ceil((rl.resetAt - Date.now()) / 1000)) } }
);
}
try {
const body = await req.json();
const { email } = body as { email?: unknown };
if (typeof email !== "string" || !email.trim()) {
return badRequest("Email is required.");
}
const normalizedEmail = email.trim().toLowerCase();
// Always return success to prevent email enumeration
const ok = { ok: true } as const;
const user = await prisma.user.findUnique({
where: { email: normalizedEmail },
});
// No user or Google-only user (no password) — silently succeed
if (!user || !user.password) {
return NextResponse.json(ok);
}
// Generate token anPOST function · typescript · L7-L67 (61 LOC)src/app/api/auth/reset-password/route.ts
export async function POST(req: NextRequest) {
const ip = getClientIp(req);
const rl = rateLimit(`reset-password:${ip}`, 10, 15 * 60 * 1000);
if (!rl.allowed) {
return NextResponse.json(
{ error: "Too many requests. Please try again later." },
{ status: 429, headers: { "Retry-After": String(Math.ceil((rl.resetAt - Date.now()) / 1000)) } }
);
}
try {
const body = await req.json();
const { token, newPassword } = body as {
token?: unknown;
newPassword?: unknown;
};
if (typeof token !== "string" || !token.trim()) {
return badRequest("Token is required.");
}
if (typeof newPassword !== "string" || !newPassword) {
return badRequest("New password is required.");
}
if (newPassword.length < 8) {
return badRequest("Password must be at least 8 characters.");
}
if (newPassword.length > 128) {
return badRequest("Password is too long.");
}
const resetRecord = await prisma.passwordResetPOST function · typescript · L14-L72 (59 LOC)src/app/api/auth/send-verification/route.ts
export async function POST(req: NextRequest) {
const ip = getClientIp(req);
const rl = rateLimit(`send-verification:${ip}`, 5, 15 * 60 * 1000);
if (!rl.allowed) {
return tooManyRequests(undefined, rl.resetAt - Date.now());
}
try {
const body = await req.json();
const { email, name, password } = body as {
email?: unknown;
name?: unknown;
password?: unknown;
};
if (
typeof email !== "string" ||
typeof name !== "string" ||
typeof password !== "string"
) {
return badRequest("Invalid request body.");
}
const normalizedEmail = email.trim().toLowerCase();
const trimmedName = name.trim();
if (!trimmedName) return badRequest("Name is required.");
if (!normalizedEmail || !EMAIL_RE.test(normalizedEmail))
return badRequest("A valid email address is required.");
if (password.length < 8)
return badRequest("Password must be at least 8 characters.");
// Check if email already registPOST function · typescript · L9-L85 (77 LOC)src/app/api/auth/signup/route.ts
export async function POST(req: NextRequest) {
const ip = getClientIp(req);
const rl = rateLimit(`signup:${ip}`, 10, 15 * 60 * 1000);
if (!rl.allowed) {
return NextResponse.json(
{ error: "Too many requests. Please try again later." },
{ status: 429, headers: { "Retry-After": String(Math.ceil((rl.resetAt - Date.now()) / 1000)) } }
);
}
try {
const body = await req.json();
const { name, email, password, code } = body as {
name?: unknown;
email?: unknown;
password?: unknown;
code?: unknown;
};
if (
typeof name !== "string" ||
typeof email !== "string" ||
typeof password !== "string" ||
typeof code !== "string"
) {
return badRequest("Invalid request body.");
}
const trimmedName = name.trim();
const normalizedEmail = email.trim().toLowerCase();
const trimmedCode = code.trim();
if (!trimmedName) return badRequest("Name is required.");
if (trimmedName.length > 1GET function · typescript · L7-L40 (34 LOC)src/app/api/market/history/route.ts
export async function GET(req: NextRequest) {
const userId = await getSessionUserId();
if (!userId) return unauthorizedResponse();
try {
const symbol = req.nextUrl.searchParams.get("symbol")?.trim();
if (!symbol) {
return badRequest("symbol query param required");
}
if (symbol.length > 20) {
return badRequest("Symbol too long.");
}
const rawRange = req.nextUrl.searchParams.get("range") ?? "1y";
const range: HistoryRange = VALID_RANGES.includes(rawRange as HistoryRange)
? (rawRange as HistoryRange)
: "1y";
const result = await getHistoricalData(symbol, range);
if (!result.data) {
return NextResponse.json(
{ error: result.error ?? "Failed to fetch history" },
{ status: 502 }
);
}
return NextResponse.json(result, {
headers: result.stale ? { "X-Cache": "STALE" } : { "X-Cache": "MISS" },
});
} catch {
return serverError();
}
}GET function · typescript · L5-L33 (29 LOC)src/app/api/market/quote/route.ts
export async function GET(req: NextRequest) {
const userId = await getSessionUserId();
if (!userId) return unauthorizedResponse();
try {
const symbol = req.nextUrl.searchParams.get("symbol")?.trim();
if (!symbol) {
return badRequest("symbol query param required");
}
if (symbol.length > 20) {
return badRequest("Symbol too long.");
}
const result = await getQuote(symbol);
if (!result.data) {
return NextResponse.json(
{ error: result.error ?? "Failed to fetch quote" },
{ status: 502 }
);
}
return NextResponse.json(result, {
headers: result.stale ? { "X-Cache": "STALE" } : { "X-Cache": "MISS" },
});
} catch {
return serverError();
}
}GET function · typescript · L5-L16 (12 LOC)src/app/api/market/search/route.ts
export async function GET(req: NextRequest) {
const userId = await getSessionUserId();
if (!userId) return unauthorizedResponse();
try {
const q = req.nextUrl.searchParams.get("q")?.trim().slice(0, 100) ?? "";
const results = await searchSymbols(q);
return NextResponse.json(results);
} catch {
return serverError();
}
}DELETE function · typescript · L6-L34 (29 LOC)src/app/api/portfolios/[id]/route.ts
export async function DELETE(
_req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const userId = await getSessionUserId();
if (!userId) return unauthorizedResponse();
try {
const { id } = await params;
const portfolio = await prisma.portfolio.findFirst({
where: { id, userId },
include: { _count: { select: { assets: true } } },
});
if (!portfolio) {
return notFound("Portfolio not found.");
}
if (portfolio._count.assets > 0) {
return badRequest("Cannot delete a portfolio that still has assets. Remove all assets first.");
}
await deletePortfolio(id);
return new NextResponse(null, { status: 204 });
} catch {
return serverError();
}
}All rows above produced by Repobility · https://repobility.com
GET function · typescript · L6-L11 (6 LOC)src/app/api/portfolios/route.ts
export async function GET() {
const userId = await getSessionUserId();
if (!userId) return unauthorizedResponse();
const portfolios = await getPortfolios(userId);
return NextResponse.json(portfolios);
}POST function · typescript · L13-L40 (28 LOC)src/app/api/portfolios/route.ts
export async function POST(req: NextRequest) {
const userId = await getSessionUserId();
if (!userId) return unauthorizedResponse();
try {
const { name } = await req.json();
if (!name || typeof name !== "string" || !name.trim()) {
return badRequest("Portfolio name is required.");
}
const trimmed = name.trim();
// Check for duplicate name (SQLite LIKE is case-insensitive by default for ASCII)
const existing = await prisma.portfolio.findFirst({
where: {
userId,
name: { equals: trimmed },
},
});
if (existing) {
return conflictResponse("A portfolio with this name already exists.");
}
const portfolio = await createPortfolio({ name: trimmed, userId });
return NextResponse.json(portfolio, { status: 201 });
} catch {
return serverError();
}
}escapeHtml function · typescript · L7-L14 (8 LOC)src/app/api/support/route.ts
function escapeHtml(str: string): string {
return str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}POST function · typescript · L16-L64 (49 LOC)src/app/api/support/route.ts
export async function POST(req: NextRequest) {
const userId = await getSessionUserId();
if (!userId) return unauthorizedResponse();
const rl = rateLimit(`support:${userId}`, 3, 60 * 60 * 1000);
if (!rl.allowed) {
return tooManyRequests(undefined, rl.resetAt - Date.now());
}
try {
// We need the full session for user name/email in the email
const session = await auth();
const body = await req.json();
const { subject, message } = body as { subject?: unknown; message?: unknown };
if (typeof subject !== "string" || typeof message !== "string") {
return badRequest("Invalid request.");
}
if (!subject.trim()) return badRequest("Subject is required.");
if (!message.trim()) return badRequest("Message is required.");
if (message.length > 2000) return badRequest("Message is too long.");
const resend = new Resend(process.env.RESEND_API_KEY);
const { error } = await resend.emails.send({
from: "FolioVault <noreply@foliovaulPOST function · typescript · L9-L69 (61 LOC)src/app/api/transactions/route.ts
export async function POST(req: NextRequest) {
const userId = await getSessionUserId();
if (!userId) return unauthorizedResponse();
try {
const body = (await req.json()) as Partial<CreateTransactionInput>;
const { assetId, type, quantity, pricePerUnit, date, fees, notes } = body;
// Required fields
if (!assetId || !type || quantity == null || pricePerUnit == null || !date) {
return badRequest("Missing required fields.");
}
// Type validation
if (!VALID_TYPES.has(type)) {
return badRequest("Invalid transaction type.");
}
if (typeof quantity !== "number" || !isFinite(quantity) || quantity <= 0) {
return badRequest("Quantity must be a positive number.");
}
if (typeof pricePerUnit !== "number" || !isFinite(pricePerUnit) || pricePerUnit < 0) {
return badRequest("Price per unit must be a non-negative number.");
}
if (fees !== undefined && fees !== null) {
if (typeof fees !== "number" || !isFinite(fees)DELETE function · typescript · L7-L54 (48 LOC)src/app/api/user/delete/route.ts
export async function DELETE(req: NextRequest) {
const userId = await getSessionUserId();
if (!userId) return unauthorizedResponse();
// Rate limit: 5 deletion attempts per user per hour
const rl = rateLimit(`delete-account:${userId}`, 5, 60 * 60 * 1000);
if (!rl.allowed) {
return tooManyRequests(undefined, rl.resetAt - Date.now());
}
// Also rate limit by IP
const ip = getClientIp(req);
const ipRl = rateLimit(`delete-account-ip:${ip}`, 10, 60 * 60 * 1000);
if (!ipRl.allowed) {
return tooManyRequests(undefined, ipRl.resetAt - Date.now());
}
try {
const body = await req.json();
const { password, confirmation } = body as { password?: unknown; confirmation?: unknown };
const user = await getUserById(userId);
if (!user) {
return notFound("User not found.");
}
if (user.password) {
// Email/password user — verify password
if (typeof password !== "string" || !password) {
return badRequest("Password is requiGET function · typescript · L18-L117 (100 LOC)src/app/api/user/export/route.ts
export async function GET(req: NextRequest) {
const userId = await getSessionUserId();
if (!userId) return unauthorizedResponse();
try {
const rawFormat = req.nextUrl.searchParams.get("format");
const format = VALID_FORMATS.includes(rawFormat as ExportFormat)
? (rawFormat as ExportFormat)
: null;
if (!format) {
return badRequest("Invalid format. Use csv-assets, csv-transactions, or json.");
}
if (format === "csv-assets") {
const assets = await getUserAssetsFlat(userId);
const headers = [
"Portfolio",
"Symbol",
"Name",
"Type",
"Quantity",
"Avg Buy Price",
"Currency",
"Notes",
"Added",
];
const rows = assets.map((a) =>
[
a.portfolioName,
a.symbol,
a.name,
a.assetType,
a.quantity,
a.averageBuyPrice,
a.currency,
a.notes ?? "",
a.createdAt.toISOString().PATCH function · typescript · L7-L64 (58 LOC)src/app/api/user/password/route.ts
export async function PATCH(req: NextRequest) {
const userId = await getSessionUserId();
if (!userId) return unauthorizedResponse();
// Rate limit: 5 password change attempts per user per hour
const rl = rateLimit(`change-password:${userId}`, 5, 60 * 60 * 1000);
if (!rl.allowed) {
return tooManyRequests(undefined, rl.resetAt - Date.now());
}
try {
const body = await req.json();
const { currentPassword, newPassword } = body as {
currentPassword?: unknown;
newPassword?: unknown;
};
if (typeof newPassword !== "string" || !newPassword) {
return badRequest("New password is required.");
}
if (newPassword.length < 8) {
return badRequest("New password must be at least 8 characters.");
}
if (newPassword.length > 128) {
return badRequest("New password is too long.");
}
const user = await getUserById(userId);
if (!user) {
return notFound("User not found.");
}
if (!user.password) {
Repobility — same analyzer, your code, free for public repos · /scan/
PATCH function · typescript · L12-L79 (68 LOC)src/app/api/user/profile/route.ts
export async function PATCH(req: NextRequest) {
const userId = await getSessionUserId();
if (!userId) return unauthorizedResponse();
try {
const body = await req.json();
const { name, email, defaultCurrency } = body as {
name?: unknown;
email?: unknown;
defaultCurrency?: unknown;
};
const updates: { name?: string; email?: string; defaultCurrency?: string } = {};
if (name !== undefined) {
if (typeof name !== "string" || name.trim().length === 0) {
return badRequest("Name cannot be empty.");
}
if (name.trim().length > 100) {
return badRequest("Name must be 100 characters or fewer.");
}
updates.name = name.trim();
}
if (email !== undefined) {
if (typeof email !== "string") {
return badRequest("Invalid email.");
}
const normalizedEmail = email.trim().toLowerCase();
if (!EMAIL_RE.test(normalizedEmail)) {
return badRequest("A valid email address is reqDELETE function · typescript · L5-L19 (15 LOC)src/app/api/watchlist/[id]/route.ts
export async function DELETE(
_req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const userId = await getSessionUserId();
if (!userId) return unauthorizedResponse();
try {
const { id } = await params;
await removeFromWatchlist(id, userId);
return new NextResponse(null, { status: 204 });
} catch {
return serverError();
}
}GET function · typescript · L7-L17 (11 LOC)src/app/api/watchlist/route.ts
export async function GET() {
const userId = await getSessionUserId();
if (!userId) return unauthorizedResponse();
try {
const items = await getWatchlistByUser(userId);
return NextResponse.json(items);
} catch {
return serverError();
}
}POST function · typescript · L19-L45 (27 LOC)src/app/api/watchlist/route.ts
export async function POST(req: NextRequest) {
const userId = await getSessionUserId();
if (!userId) return unauthorizedResponse();
try {
const body = (await req.json()) as Partial<CreateWatchlistItemInput>;
const { symbol, name, assetType } = body;
if (!symbol || typeof symbol !== "string" || symbol.trim().length === 0 || symbol.length > 20) {
return badRequest("Invalid symbol.");
}
if (!name || typeof name !== "string" || name.trim().length === 0) {
return badRequest("Name is required.");
}
if (!assetType || typeof assetType !== "string") {
return badRequest("Asset type is required.");
}
const item = await addToWatchlist(userId, body as CreateWatchlistItemInput);
return NextResponse.json(item, { status: 201 });
} catch (err: unknown) {
if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002") {
return conflictResponse("This symbol is already in your watchlist.");
}
return serForgotPasswordPage function · typescript · L19-L126 (108 LOC)src/app/(auth)/forgot-password/page.tsx
export default function ForgotPasswordPage() {
const [email, setEmail] = useState("");
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [submitted, setSubmitted] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError(null);
const trimmed = email.trim();
if (!trimmed) {
setError("Email is required.");
return;
}
if (!EMAIL_RE.test(trimmed)) {
setError("Enter a valid email address.");
return;
}
setLoading(true);
try {
const res = await fetch("/api/auth/forgot-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: trimmed }),
});
if (!res.ok) {
const data = await res.json();
setError(data.error ?? "Something went wrong.");
return;
}
setSubmitted(true);
} finally {
setLoading(handleSubmit function · typescript · L25-L57 (33 LOC)src/app/(auth)/forgot-password/page.tsx
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError(null);
const trimmed = email.trim();
if (!trimmed) {
setError("Email is required.");
return;
}
if (!EMAIL_RE.test(trimmed)) {
setError("Enter a valid email address.");
return;
}
setLoading(true);
try {
const res = await fetch("/api/auth/forgot-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: trimmed }),
});
if (!res.ok) {
const data = await res.json();
setError(data.error ?? "Something went wrong.");
return;
}
setSubmitted(true);
} finally {
setLoading(false);
}
}AuthLayout function · typescript · L10-L49 (40 LOC)src/app/(auth)/layout.tsx
export default function AuthLayout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen flex">
{/* Left branding panel — hidden on mobile */}
<div className="hidden lg:flex lg:w-1/2 bg-gradient-to-br from-primary to-primary/80 text-primary-foreground flex-col justify-center px-12 xl:px-20">
<div className="max-w-md space-y-6">
<div className="flex items-center gap-2.5">
<FolioVaultLogo size={28} />
<span className="text-2xl font-bold tracking-tight">FolioVault</span>
</div>
<p className="text-lg font-medium leading-relaxed opacity-90">
Your investments, one clear view. Track, analyse, and stay on top of
every asset in your portfolio.
</p>
<ul className="space-y-3 pt-2">
{HIGHLIGHTS.map(({ icon: Icon, text }) => (
<li key={text} className="flex items-center gap-3 text-sm opacity-80">
<Icon LoginPage function · typescript · L21-L163 (143 LOC)src/app/(auth)/login/page.tsx
export default function LoginPage() {
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
function validate(): string | null {
if (!email.trim()) return "Email is required.";
if (!EMAIL_RE.test(email)) return "Enter a valid email address.";
if (!password) return "Password is required.";
return null;
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const validationError = validate();
if (validationError) {
setError(validationError);
return;
}
setLoading(true);
setError(null);
const result = await signIn("credentials", {
email: email.trim().toLowerCase(),
password,
redirect: false,
});
setLoading(false);
if (result?.error) {
setError("Invalid email or password.");
return;
}
rPowered by Repobility — scan your code at https://repobility.com
validate function · typescript · L28-L33 (6 LOC)src/app/(auth)/login/page.tsx
function validate(): string | null {
if (!email.trim()) return "Email is required.";
if (!EMAIL_RE.test(email)) return "Enter a valid email address.";
if (!password) return "Password is required.";
return null;
}handleSubmit function · typescript · L35-L61 (27 LOC)src/app/(auth)/login/page.tsx
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const validationError = validate();
if (validationError) {
setError(validationError);
return;
}
setLoading(true);
setError(null);
const result = await signIn("credentials", {
email: email.trim().toLowerCase(),
password,
redirect: false,
});
setLoading(false);
if (result?.error) {
setError("Invalid email or password.");
return;
}
router.push("/dashboard");
router.refresh();
}ResetPasswordForm function · typescript · L19-L144 (126 LOC)src/app/(auth)/reset-password/page.tsx
function ResetPasswordForm() {
const searchParams = useSearchParams();
const router = useRouter();
const token = searchParams.get("token") ?? "";
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError(null);
if (!token) {
setError("Missing reset token. Please use the link from your email.");
return;
}
if (newPassword.length < 8) {
setError("Password must be at least 8 characters.");
return;
}
if (newPassword !== confirmPassword) {
setError("Passwords do not match.");
return;
}
setLoading(true);
try {
const res = await fetch("/api/auth/reset-password", {
method: "POST",
headers: { "ContehandleSubmit function · typescript · L30-L66 (37 LOC)src/app/(auth)/reset-password/page.tsx
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError(null);
if (!token) {
setError("Missing reset token. Please use the link from your email.");
return;
}
if (newPassword.length < 8) {
setError("Password must be at least 8 characters.");
return;
}
if (newPassword !== confirmPassword) {
setError("Passwords do not match.");
return;
}
setLoading(true);
try {
const res = await fetch("/api/auth/reset-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token, newPassword }),
});
const data = await res.json();
if (!res.ok) {
setError(data.error ?? "Something went wrong.");
return;
}
setSuccess(true);
setTimeout(() => router.push("/login"), 3000);
} finally {
setLoading(false);
}
}ResetPasswordPage function · typescript · L146-L160 (15 LOC)src/app/(auth)/reset-password/page.tsx
export default function ResetPasswordPage() {
return (
<Suspense
fallback={
<Card className="w-full max-w-sm shadow-md">
<CardContent className="py-8 text-center text-sm text-muted-foreground">
Loading...
</CardContent>
</Card>
}
>
<ResetPasswordForm />
</Suspense>
);
}validateDetails function · typescript · L55-L62 (8 LOC)src/app/(auth)/signup/page.tsx
function validateDetails(): string | null {
if (!name.trim()) return "Name is required.";
if (!email.trim() || !EMAIL_RE.test(email)) return "Enter a valid email address.";
if (password.length < 8) return "Password must be at least 8 characters.";
if (password !== confirm) return "Passwords do not match.";
if (!agreed) return "You must agree to the Terms of Service and Privacy Policy.";
return null;
}handleSendCode function · typescript · L64-L94 (31 LOC)src/app/(auth)/signup/page.tsx
async function handleSendCode(e: React.FormEvent) {
e.preventDefault();
const err = validateDetails();
if (err) { setError(err); return; }
setError(null);
setLoading(true);
const res = await fetch("/api/auth/send-verification", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: name.trim(),
email: email.trim().toLowerCase(),
password,
}),
});
setLoading(false);
if (!res.ok) {
const body = await res.json().catch(() => ({}));
setError((body as { error?: string }).error ?? "Failed to send code. Please try again.");
return;
}
setDigits(Array(6).fill(""));
setStep("verify");
startCooldown();
// Focus first digit input on next tick
setTimeout(() => digitRefs.current[0]?.focus(), 50);
}handleDigitChange function · typescript · L98-L114 (17 LOC)src/app/(auth)/signup/page.tsx
function handleDigitChange(index: number, value: string) {
// Allow paste of full code
if (value.length > 1) {
const pasted = value.replace(/\D/g, "").slice(0, 6).split("");
const next = Array(6).fill("");
pasted.forEach((d, i) => { next[i] = d; });
setDigits(next);
const focusIdx = Math.min(pasted.length, 5);
digitRefs.current[focusIdx]?.focus();
return;
}
if (value && !/^\d$/.test(value)) return; // digits only
const next = [...digits];
next[index] = value;
setDigits(next);
if (value && index < 5) digitRefs.current[index + 1]?.focus();
}Repobility (the analyzer behind this table) · https://repobility.com
handleVerify function · typescript · L122-L149 (28 LOC)src/app/(auth)/signup/page.tsx
async function handleVerify(e: React.FormEvent) {
e.preventDefault();
const code = digits.join("");
if (code.length < 6) { setError("Please enter the full 6-digit code."); return; }
setError(null);
setLoading(true);
const res = await fetch("/api/auth/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: name.trim(),
email: email.trim().toLowerCase(),
password,
code,
}),
});
setLoading(false);
if (!res.ok) {
const body = await res.json().catch(() => ({}));
setError((body as { error?: string }).error ?? "Sign up failed. Please try again.");
return;
}
router.push("/login");
}handleResend function · typescript · L151-L177 (27 LOC)src/app/(auth)/signup/page.tsx
async function handleResend() {
if (resendCooldown > 0) return;
setError(null);
setLoading(true);
const res = await fetch("/api/auth/send-verification", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: name.trim(),
email: email.trim().toLowerCase(),
password,
}),
});
setLoading(false);
if (!res.ok) {
const body = await res.json().catch(() => ({}));
setError((body as { error?: string }).error ?? "Failed to resend code.");
return;
}
setDigits(Array(6).fill(""));
digitRefs.current[0]?.focus();
startCooldown();
}startCooldown function · typescript · L179-L188 (10 LOC)src/app/(auth)/signup/page.tsx
function startCooldown() {
setResendCooldown(60);
if (cooldownRef.current) clearInterval(cooldownRef.current);
cooldownRef.current = setInterval(() => {
setResendCooldown((n) => {
if (n <= 1) { if (cooldownRef.current) clearInterval(cooldownRef.current); return 0; }
return n - 1;
});
}, 1000);
}AlertsLoading function · typescript · L3-L31 (29 LOC)src/app/dashboard/alerts/loading.tsx
export default function AlertsLoading() {
return (
<div className="container py-8 space-y-6">
{/* Page header */}
<Skeleton className="h-8 w-24" />
{/* Action bar */}
<div className="flex justify-between">
<Skeleton className="h-4 w-20" />
<div className="flex gap-2">
<Skeleton className="h-9 w-9" />
<Skeleton className="h-9 w-28" />
</div>
</div>
{/* Table rows */}
<div className="rounded-md border p-4 space-y-3">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="flex gap-4">
<Skeleton className="h-4 w-16" />
<Skeleton className="h-4 w-16" />
<Skeleton className="h-4 flex-1" />
<Skeleton className="h-4 w-20" />
</div>
))}
</div>
</div>
);
}AlertsPage function · typescript · L12-L50 (39 LOC)src/app/dashboard/alerts/page.tsx
export default async function AlertsPage() {
const session = await auth();
if (!session?.user?.id) redirect("/login");
const [dbAlerts, dbAssets] = await Promise.all([
getAlertsByUser(session.user.id),
getAssetsByUser(session.user.id),
]);
const alerts: PriceAlertRow[] = dbAlerts.map((a) => ({
id: a.id,
assetId: a.assetId,
symbol: a.symbol,
assetName: a.asset.name,
condition: a.condition as "ABOVE" | "BELOW",
targetPrice: a.targetPrice,
active: a.active,
triggeredAt: a.triggeredAt?.toISOString() ?? null,
createdAt: a.createdAt.toISOString(),
}));
const assetOptions: AssetOption[] = dbAssets.map((a) => ({
id: a.id,
symbol: a.symbol,
name: a.name,
portfolioName: a.portfolio.name,
}));
return (
<main className="container py-8 space-y-6">
<PageHeader
icon={Bell}
title="Price Alerts"
description="Monitor price thresholds for your assets."
/>
<AlertsTable initialAleAnalyticsPage function · typescript · L14-L50 (37 LOC)src/app/dashboard/analytics/page.tsx
export default async function AnalyticsPage() {
const session = await auth();
if (!session?.user?.id) redirect("/login");
const currency = session.user.defaultCurrency ?? "USD";
const data = await getDashboardData(session.user.id, currency);
return (
<main className="container py-8 space-y-6">
<PageHeader
icon={BarChart2}
title="Analytics"
description="Deep-dive into your portfolio performance and allocation."
/>
<SummaryCards
totalValue={data.totalValue}
totalCost={data.totalCost}
totalGainLoss={data.totalGainLoss}
totalGainLossPct={data.totalGainLossPct}
currency={currency}
/>
<PerformanceChart performance={data.performance} />
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<AllocationChart
byType={data.allocationByType}
byAsset={data.allocationByAsset}
/>
<PnlBarChart assets={data.allAssets} currency={currency} />AssetDetailLoading function · typescript · L3-L23 (21 LOC)src/app/dashboard/assets/[id]/loading.tsx
export default function AssetDetailLoading() {
return (
<div className="container py-8 space-y-6">
{/* Header */}
<div className="flex items-center gap-3">
<Skeleton className="h-5 w-28" />
<Skeleton className="h-8 w-48" />
<Skeleton className="h-5 w-16" />
</div>
{/* Chart placeholder */}
<Skeleton className="h-[65vh] w-full rounded-lg" />
{/* Bottom cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Skeleton className="h-48 rounded-lg" />
<Skeleton className="h-48 rounded-lg" />
</div>
</div>
);
}AssetDetailPage function · typescript · L12-L70 (59 LOC)src/app/dashboard/assets/[id]/page.tsx
export default async function AssetDetailPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const session = await auth();
if (!session?.user?.id) redirect("/login");
const { id } = await params;
const asset = await getAssetById(id, session.user.id);
if (!asset) notFound();
const marketSymbol = toMarketSymbol(asset.symbol, asset.assetType);
const [quoteResult, dbAlerts] = await Promise.all([
getQuote(marketSymbol),
getAlertsBySymbol(session.user.id, asset.symbol),
]);
const currentPrice = quoteResult.data?.price ?? null;
const effectivePrice = currentPrice ?? asset.averageBuyPrice;
const marketValue = asset.quantity * effectivePrice;
const costBasis = asset.quantity * asset.averageBuyPrice;
const pnl = marketValue - costBasis;
const pnlPct = costBasis > 0 ? pnl / costBasis : 0;
const tvSymbol = toTradingViewSymbol(asset.symbol, asset.assetType);
const alerts: PriceAlertRow[] = dbAlerts.map((a) => ({
id: a.id,
assetId: a.assAll rows above produced by Repobility · https://repobility.com
AssetsLoading function · typescript · L3-L32 (30 LOC)src/app/dashboard/assets/loading.tsx
export default function AssetsLoading() {
return (
<div className="container py-8 space-y-6">
{/* Page header */}
<Skeleton className="h-8 w-40" />
{/* Toolbar */}
<div className="flex gap-3">
<Skeleton className="h-9 w-64" />
<Skeleton className="h-9 w-32" />
<div className="ml-auto flex gap-2">
<Skeleton className="h-9 w-9" />
<Skeleton className="h-9 w-24" />
</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-16" />
<Skeleton className="h-4 flex-1" />
<Skeleton className="h-4 w-20" />
<Skeleton className="h-4 w-24" />
</div>
))}
</div>
</div>
);
}AssetsPage function · typescript · L12-L101 (90 LOC)src/app/dashboard/assets/page.tsx
export default async function AssetsPage() {
const session = await auth();
if (!session?.user?.id) redirect("/login");
const currency = session.user.defaultCurrency ?? "USD";
const [dbAssets, portfolios] = await Promise.all([
getAssetsByUser(session.user.id),
getPortfolios(session.user.id),
]);
const symbols = dbAssets.map((a) => toMarketSymbol(a.symbol, a.assetType));
const [quotes, fxRate] = await Promise.all([
getBatchQuotes(symbols),
getFXRate(currency),
]);
const enrichedAssets: EnrichedAsset[] = dbAssets.map((a) => {
const sym = toMarketSymbol(a.symbol, a.assetType);
const quote = quotes.get(sym);
const currentPrice = quote ? quote.price * fxRate : null;
const effectivePrice = currentPrice ?? a.averageBuyPrice * fxRate;
const marketValue = a.quantity * effectivePrice;
const costBasis = a.quantity * a.averageBuyPrice * fxRate;
return {
id: a.id,
ids: [a.id],
symbol: a.symbol,
name: a.name,
ChartPage function · typescript · L17-L134 (118 LOC)src/app/dashboard/chart/[symbol]/page.tsx
export default async function ChartPage({
params,
searchParams,
}: {
params: Promise<{ symbol: string }>;
searchParams: Promise<{ type?: string }>;
}) {
const session = await auth();
if (!session?.user?.id) redirect("/login");
const { symbol: rawSymbol } = await params;
const { type } = await searchParams;
const assetType = type || "STOCK";
const symbol = decodeURIComponent(rawSymbol);
const tvSymbol = toTradingViewSymbol(symbol, assetType);
// Look up whether the user holds this symbol and fetch alerts
const [asset, dbAlerts] = await Promise.all([
getAssetBySymbol(symbol, session.user.id),
getAlertsBySymbol(session.user.id, symbol),
]);
// If the user holds this asset, fetch a live price for holdings display
let holdingsProps: {
id: string;
symbol: string;
name: string;
assetType: string;
portfolioName: string;
quantity: number;
averageBuyPrice: number;
currentPrice: number | null;
marketValue: number;
page 1 / 5next ›