Skip to content
Merged
21 changes: 0 additions & 21 deletions .claude/settings.local.json

This file was deleted.

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,7 @@ Thumbs.db
.idea/
*.swp

# Claude Code local settings
.claude/

desktop.ini
58 changes: 42 additions & 16 deletions src/app/api/goals/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ interface Goal {

type Recurrence = "none" | "weekly" | "monthly";

const VALID_RECURRENCES = ["none", "weekly", "monthly"] as const;
const MAX_TITLE_LEN = 100;
const MAX_UNIT_LEN = 30;
const MIN_TARGET = 1;
const MAX_TARGET = 10_000;

function getPeriodStart(recurrence: Recurrence): string {
const now = new Date();
if (recurrence === "weekly") {
Expand Down Expand Up @@ -106,21 +112,41 @@ export async function POST(req: Request) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

const body = (await req.json()) as {
title?: string;
target?: number;
unit?: string;
recurrence?: Recurrence;
};
let body: unknown;
try {
body = await req.json();
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}

if (!body.title || typeof body.target !== "number" || body.target <= 0) {
return Response.json({ error: "title and positive target required" }, { status: 400 });
if (typeof body !== "object" || body === null) {
return Response.json({ error: "Invalid request body" }, { status: 400 });
}

const recurrence: Recurrence = body.recurrence ?? "none";
if (!["none", "weekly", "monthly"].includes(recurrence)) {
return Response.json({ error: "Invalid recurrence value" }, { status: 400 });
const { title, target, unit, recurrence } = body as Record<string, unknown>;

if (typeof title !== "string" || title.trim().length === 0) {
return Response.json({ error: "title must be a non-empty string" }, { status: 400 });
}
if (title.length > MAX_TITLE_LEN) {
return Response.json({ error: `title must be ${MAX_TITLE_LEN} characters or fewer` }, { status: 400 });
}
if (
typeof target !== "number" ||
!Number.isInteger(target) ||
target < MIN_TARGET ||
target > MAX_TARGET
) {
return Response.json(
{ error: `target must be an integer between ${MIN_TARGET} and ${MAX_TARGET}` },
{ status: 400 }
);
}

const safeUnit = typeof unit === "string" ? unit.slice(0, MAX_UNIT_LEN) : "commits";
const safeRecurrence: Recurrence = VALID_RECURRENCES.includes(recurrence as Recurrence)
? (recurrence as Recurrence)
: "none";

const user = await resolveAppUser(session.githubId, session.githubLogin);
if (!user) return Response.json({ error: "User not found" }, { status: 404 });
Expand All @@ -129,11 +155,11 @@ export async function POST(req: Request) {
.from("goals")
.insert({
user_id: user.id,
title: body.title,
target: body.target,
unit: body.unit ?? "commits",
recurrence,
period_start: getPeriodStart(recurrence),
title: title.trim(),
target,
unit: safeUnit,
recurrence: safeRecurrence,
period_start: getPeriodStart(safeRecurrence),
current: 0,
})
.select()
Expand Down
8 changes: 4 additions & 4 deletions src/components/CommitTimeChart.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import {
BarChart,
Bar,
Expand All @@ -27,7 +27,7 @@ export default function CommitTimeChart() {
const [days, setDays] = useState(30);
const [peakTime, setPeakTime] = useState<string | null>(null);

const fetchContributions = () => {
const fetchContributions = useCallback(() => {
setLoading(true);
setError(null);
fetch(`/api/metrics/contributions?days=${days}`)
Expand Down Expand Up @@ -81,11 +81,11 @@ export default function CommitTimeChart() {
setError("We couldn't load your time-of-day data right now."),
)
.finally(() => setLoading(false));
};
}, [days]);

useEffect(() => {
fetchContributions();
}, [days]);
}, [fetchContributions]);

return (
<div className="rounded-xl border border-[var(--border)] bg-[var(--card)] p-6 shadow-sm flex flex-col h-full">
Expand Down
Loading