← back to idaturgutinal__portfolio-tracker

Function bodies 201 total

All specs Real LLM only Function bodies
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.");
      }
      bod
DELETE 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" || !i
Repobility (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 an
POST 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.passwordReset
POST 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 regist
POST 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 > 1
GET 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, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}
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@foliovaul
POST 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 requi
GET 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 req
DELETE 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 ser
ForgotPasswordPage 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;
    }

    r
Powered 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: { "Conte
handleSubmit 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 initialAle
AnalyticsPage 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.ass
All 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 ›