Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ jobs:
runs-on: ubuntu-latest
env:
NEXTAUTH_SECRET: test-nextauth-secret-for-playwright-tests
NEXTAUTH_URL: http://127.0.0.1:3000
NEXT_PUBLIC_APP_URL: http://127.0.0.1:3000
NEXTAUTH_URL: http://127.0.0.1:3002
NEXT_PUBLIC_APP_URL: http://127.0.0.1:3002
GITHUB_ID: playwright-github-id
GITHUB_SECRET: playwright-github-secret
NEXT_PUBLIC_SUPABASE_URL: https://placeholder.supabase.co
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@ on:
pull_request_target:
jobs:
label:
if: github.event.pull_request.head.repo.full_name == github.repository
permissions:
contents: read
pull-requests: write
issues: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/labeler@v5
continue-on-error: true
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
configuration-path: .github/labeler.yml
3 changes: 2 additions & 1 deletion e2e/dashboard-widgets.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ test("dashboard widgets render with mocked metrics", async ({ page }) => {
await expect(page.getByRole("heading", { name: /dashboard/i })).toBeVisible({ timeout: 30000 });
await expect(page.getByRole("heading", { name: "Your Commits" })).toBeVisible({ timeout: 10000 });
await expect(page.getByRole("heading", { name: "PR Analytics" })).toBeVisible({ timeout: 10000 });
await expect(page.getByRole("heading", { name: "Goals" })).toBeVisible({ timeout: 10000 });
await expect(page.getByRole("heading", { name: "Goals", exact: true })).toBeVisible({ timeout: 10000 });
await expect(page.getByText("Make 10 commits")).toBeVisible({ timeout: 10000 });
});

Expand All @@ -193,6 +193,7 @@ test("contribution graph range buttons request a new range", async ({ page }) =>

await page.goto("/dashboard", { waitUntil: "load" });
await expect(page.getByRole("heading", { name: /dashboard/i })).toBeVisible({ timeout: 30000 });
await page.getByRole("button", { name: "Show 90-day range" }).first().click();
await page
.locator("#contribution-activity")
.getByRole("button", { name: "Show 90-day range" })
Expand Down
3 changes: 2 additions & 1 deletion e2e/theme.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ test.beforeEach(async ({ page }) => {
httpOnly: true,
sameSite: "Lax",
secure: false,
expires: Math.floor(Date.now() / 1000) + 60 * 60,
},
]);

Expand Down Expand Up @@ -59,7 +60,7 @@ test("theme toggle switches between dark and light mode", async ({ page }) => {

const initialPressed = await themeToggle.getAttribute("aria-pressed");

await themeToggle.click();
await themeToggle.click({ force: true });

await expect(themeToggle).toHaveAttribute(
"aria-pressed",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"build": "next build && node scripts/copy-standalone-static.js",
"start": "next start",
"lint": "next lint",
"type-check": "tsc --noEmit",
Expand Down
2 changes: 1 addition & 1 deletion playwright.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { defineConfig, devices } from "@playwright/test";

const PORT = Number(process.env.PORT ?? 3000);
const PORT = Number(process.env.PORT ?? 3002);
const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? `http://127.0.0.1:${PORT}`;
const prepareStandaloneCommand =
"node -e \"const fs=require('fs'); fs.cpSync('public','.next/standalone/public',{recursive:true,force:true}); fs.cpSync('.next/static','.next/standalone/.next/static',{recursive:true,force:true});\"";
Expand Down
34 changes: 34 additions & 0 deletions scripts/copy-standalone-static.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const fs = require('fs');
const path = require('path');

function copyDir(src, dest) {
if (!fs.existsSync(src)) return;
fs.mkdirSync(dest, { recursive: true });
const entries = fs.readdirSync(src, { withFileTypes: true });

for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);

if (entry.isDirectory()) {
copyDir(srcPath, destPath);
} else {
fs.copyFileSync(srcPath, destPath);
}
}
}

const standaloneDir = path.join(__dirname, '..', '.next', 'standalone');

if (fs.existsSync(standaloneDir)) {
console.log('Copying static files to standalone directory...');
copyDir(
path.join(__dirname, '..', 'public'),
path.join(standaloneDir, 'public')
);
copyDir(
path.join(__dirname, '..', '.next', 'static'),
path.join(standaloneDir, '.next', 'static')
);
console.log('Done.');
}
2 changes: 1 addition & 1 deletion src/app/api/contact/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export async function POST(request: NextRequest) {

try {
payload = (await request.json()) as ContactPayload;
} catch {
} catch (e) {
return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
}

Expand Down
83 changes: 73 additions & 10 deletions src/app/api/goals/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,6 @@ export async function PATCH(
const user = await resolveAppUser(session.githubId, session.githubLogin);
if (!user) return Response.json({ error: "User not found" }, { status: 404 });

const body = await req.json().catch(() => ({}));
const { current } = body;

if (typeof current !== "number" || current < 0) {
return Response.json(
{ error: "Invalid current value" },
{ status: 400 }
);
}

const { data: existingGoal } = await supabaseAdmin
.from("goals")
Expand All @@ -39,10 +30,82 @@ export async function PATCH(
return Response.json({ error: "Goal not found" }, { status: 404 });
}

let body: unknown;
try {
body = await req.json();
} catch (e) {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}

if (typeof body !== "object" || body === null) {
return Response.json({ error: "Invalid request body" }, { status: 400 });
}

const updates: Record<string, unknown> = {};

const { title, target, unit, recurrence, current } =
body as Record<string, unknown>;

if (title !== undefined) {
if (typeof title !== "string" || title.trim().length === 0) {
return Response.json({ error: "title must be a non-empty string" }, { status: 400 });
}
if (title.length > 100) {
return Response.json({ error: "title must be 100 characters or fewer" }, { status: 400 });
}
updates.title = title.trim();
}

if (target !== undefined) {
if (
typeof target !== "number" ||
!Number.isInteger(target) ||
target < 1 ||
target > 10_000
) {
return Response.json(
{ error: "target must be an integer between 1 and 10000" },
{ status: 400 }
);
}
updates.target = target;
}

if (unit !== undefined) {
if (typeof unit !== "string" || unit.trim().length === 0) {
return Response.json({ error: "unit must be a non-empty string" }, { status: 400 });
}
updates.unit = unit.trim();
}

if (recurrence !== undefined) {
if (recurrence !== "daily" && recurrence !== "weekly" && recurrence !== "monthly") {
return Response.json(
{ error: "recurrence must be 'daily', 'weekly', or 'monthly'" },
{ status: 400 }
);
}
updates.recurrence = recurrence;
}

if (current !== undefined) {
if (typeof current !== "number" || current < 0) {
return Response.json(
{ error: "Invalid current value" },
{ status: 400 }
);
}
updates.current = current;
}

if (Object.keys(updates).length === 0) {
return Response.json({ goal: existingGoal });
}

const wasCompleted = existingGoal.current >= existingGoal.target;
const { data: updatedGoal, error } = await supabaseAdmin
.from("goals")
.update({ current })
.update(updates)
.eq("id", params.id)
.eq("user_id", user.id)
.select()
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/goals/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export async function POST(req: Request) {

try {
body = await req.json();
} catch {
} catch (e) {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}

Expand Down
2 changes: 1 addition & 1 deletion src/app/api/integrations/jira/credentials/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export async function POST(req: NextRequest) {
let body: JiraCredentialsInput;
try {
body = await req.json();
} catch {
} catch (e) {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}

Expand Down
4 changes: 2 additions & 2 deletions src/app/api/integrations/jira/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export async function GET(req: NextRequest) {
);
}
decryptedToken = decrypted;
} catch {
} catch (e) {
return Response.json(
{ error: "Failed to decrypt credentials" },
{ status: 500 }
Expand All @@ -136,7 +136,7 @@ export async function GET(req: NextRequest) {
metrics,
recentIssues: issues.slice(0, 10),
});
} catch {
} catch (e) {
return Response.json(
{ error: "Failed to fetch Jira data" },
{ status: 502 }
Expand Down
4 changes: 2 additions & 2 deletions src/app/api/leaderboard/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
} from "@/lib/leaderboard";
import { cacheSet } from "@/lib/metrics-cache";

export const dynamic = "force-dynamic";
export const revalidate = 3600;

const RATE_LIMIT_REQUESTS = 20;
const RATE_LIMIT_WINDOW_MS = 60 * 1000;
Expand Down Expand Up @@ -120,7 +120,7 @@ export async function GET(req: NextRequest) {
await cacheSet(LEADERBOARD_CACHE_KEY, payload, CACHE_STALE_SECONDS);
setMemoryCachedLeaderboard(payload);
return NextResponse.json(payload);
} catch {
} catch (e) {
const cached = await cacheGet<LeaderboardPayload>(LEADERBOARD_CACHE_KEY);
if (cached) {
return NextResponse.json(cached, {
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/local-coding/keys/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export async function POST(req: NextRequest) {
let body: { name?: string };
try {
body = await req.json();
} catch {
} catch (e) {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}

Expand Down
2 changes: 1 addition & 1 deletion src/app/api/local-coding/sync/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export async function POST(req: NextRequest) {
let body: { sessions?: SessionData[] };
try {
body = await req.json();
} catch {
} catch (e) {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}

Expand Down
2 changes: 1 addition & 1 deletion src/app/api/metrics/activity/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ async function fetchFormattedActivityWithFallback(
): Promise<ActivityItem[]> {
try {
return await fetchFormattedActivity(token);
} catch {
} catch (e) {
if (!githubLogin) {
throw new Error("GitHub API error");
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/metrics/ci/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export async function GET(req: NextRequest) {
});

return Response.json(data);
} catch {
} catch (e) {
return Response.json({ error: "GitHub API error" }, { status: 502 });
}
}
6 changes: 3 additions & 3 deletions src/app/api/metrics/coding-activity-insights/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ function getRequestedTimeZone(req: NextRequest): string {
try {
new Intl.DateTimeFormat("en-US", { timeZone: raw }).format(new Date());
return raw;
} catch {
} catch (e) {
return "UTC";
}
}
Expand Down Expand Up @@ -158,7 +158,7 @@ export async function GET(req: NextRequest) {
);

return Response.json(data);
} catch {
} catch (e) {
return Response.json({ error: "GitHub API error" }, { status: 502 });
}
}
Expand Down Expand Up @@ -241,7 +241,7 @@ export async function GET(req: NextRequest) {
});

return Response.json(data);
} catch {
} catch (e) {
return Response.json({ error: "GitHub API error" }, { status: 502 });
}
}
2 changes: 1 addition & 1 deletion src/app/api/metrics/contributions/daily/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export async function GET(req: NextRequest) {
);

return Response.json(result);
} catch {
} catch (e) {
return Response.json({ error: "GitHub API error" }, { status: 502 });
}
}
2 changes: 1 addition & 1 deletion src/app/api/metrics/contributions/hourly/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export async function GET(req: NextRequest) {
);

return Response.json(result);
} catch {
} catch (e) {
return Response.json({ error: "GitHub API error" }, { status: 502 });
}
}
8 changes: 4 additions & 4 deletions src/app/api/metrics/contributions/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ export async function GET(req: NextRequest) {
repoParam
);
return Response.json(result);
} catch {
} catch (e) {
return Response.json({ error: "GitHub API error" }, { status: 502 });
}
}
Expand All @@ -394,7 +394,7 @@ export async function GET(req: NextRequest) {
});

return Response.json(merged);
} catch {
} catch (e) {
return Response.json({ error: "GitHub API error" }, { status: 502 });
}
}
Expand Down Expand Up @@ -481,7 +481,7 @@ export async function GET(req: NextRequest) {
});

return Response.json(merged);
} catch {
} catch (e) {
return Response.json({ error: "GitHub API error" }, { status: 502 });
}
}
Expand Down Expand Up @@ -513,7 +513,7 @@ export async function GET(req: NextRequest) {
fromDate
);
return Response.json(result);
} catch {
} catch (e) {
return Response.json({ error: "GitHub API error" }, { status: 502 });
}
}
Loading
Loading