Skip to content

Commit 29f9082

Browse files
committed
feat(settings): add platforms page and extract settings tabs component
- Create new SettingsTabs component to avoid duplication across settings pages - Add platforms settings page for managing code hosting platform connections - Update existing settings pages to use the shared component - Fix import in retry tests to use consolidated error exports
1 parent f6a6851 commit 29f9082

5 files changed

Lines changed: 104 additions & 37 deletions

File tree

apps/web/src/app/settings/llm-keys/page.tsx

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { createSupabaseServerClient } from "@/lib/supabase/server";
44
import { wrappedTheme } from "@/lib/theme";
55
import LLMKeysClient from "./LLMKeysClient";
66

7+
import { SettingsTabs } from "@/components/settings/SettingsTabs";
8+
79
export const metadata = {
810
title: "LLM API Keys · Settings · Vibe Coding Profiler",
911
description: "Manage your LLM API keys for AI-powered narrative generation",
@@ -39,17 +41,7 @@ export default async function LLMKeysPage() {
3941
</div>
4042

4143
{/* Settings Tabs */}
42-
<div className="flex gap-1 rounded-xl bg-zinc-100 p-1">
43-
<span className="flex-1 rounded-lg bg-white px-4 py-2 text-center text-sm font-medium text-zinc-900 shadow-sm">
44-
LLM Keys
45-
</span>
46-
<Link
47-
href="/settings/repos"
48-
className="flex-1 rounded-lg px-4 py-2 text-center text-sm font-medium text-zinc-600 transition-colors hover:text-zinc-900"
49-
>
50-
Repos
51-
</Link>
52-
</div>
44+
<SettingsTabs activeTab="llm-keys" />
5345

5446
{/* Main content */}
5547
<div className={`${wrappedTheme.card} p-6`}>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { redirect } from "next/navigation";
2+
import { createSupabaseServerClient } from "@/lib/supabase/server";
3+
import { wrappedTheme } from "@/lib/theme";
4+
import { PlatformConnections } from "@/components/settings/PlatformConnections";
5+
import { SettingsTabs } from "@/components/settings/SettingsTabs";
6+
import Link from "next/link";
7+
8+
export const metadata = {
9+
title: "Platforms · Settings · Vibe Coding Profiler",
10+
description: "Manage your connected code hosting platforms",
11+
};
12+
13+
export default async function PlatformsPage() {
14+
const supabase = await createSupabaseServerClient();
15+
const {
16+
data: { user },
17+
} = await supabase.auth.getUser();
18+
19+
if (!user) {
20+
redirect("/login");
21+
}
22+
23+
return (
24+
<div className={`${wrappedTheme.container} ${wrappedTheme.pageY}`}>
25+
<div className="mx-auto max-w-2xl space-y-8">
26+
{/* Header */}
27+
<div>
28+
<div className="flex items-center gap-2 text-sm text-zinc-600">
29+
<Link href="/settings/llm-keys" className="hover:text-zinc-900">
30+
Settings
31+
</Link>
32+
<span>/</span>
33+
<span className="text-zinc-900">Platforms</span>
34+
</div>
35+
<h1 className="mt-2 text-3xl font-semibold tracking-tight text-zinc-950">
36+
Connected Platforms
37+
</h1>
38+
<p className="mt-2 text-zinc-600">
39+
Manage your connections to GitHub, GitLab, and Bitbucket.
40+
</p>
41+
</div>
42+
43+
{/* Settings Tabs */}
44+
<SettingsTabs activeTab="platforms" />
45+
46+
{/* Main Content */}
47+
<div className={`${wrappedTheme.card} p-6`}>
48+
<PlatformConnections />
49+
</div>
50+
</div>
51+
</div>
52+
);
53+
}

apps/web/src/app/settings/repos/page.tsx

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { redirect } from "next/navigation";
22
import Link from "next/link";
33
import { createSupabaseServerClient } from "@/lib/supabase/server";
44
import { wrappedTheme } from "@/lib/theme";
5+
import { SettingsTabs } from "@/components/settings/SettingsTabs";
56
import ReposClient from "@/app/repos/ReposClient";
67

78
export const runtime = "nodejs";
@@ -72,17 +73,7 @@ export default async function RepoSettingsPage() {
7273
</div>
7374

7475
{/* Settings Tabs */}
75-
<div className="flex gap-1 rounded-xl bg-zinc-100 p-1">
76-
<Link
77-
href="/settings/llm-keys"
78-
className="flex-1 rounded-lg px-4 py-2 text-center text-sm font-medium text-zinc-600 transition-colors hover:text-zinc-900"
79-
>
80-
LLM Keys
81-
</Link>
82-
<span className="flex-1 rounded-lg bg-white px-4 py-2 text-center text-sm font-medium text-zinc-900 shadow-sm">
83-
Repos
84-
</span>
85-
</div>
76+
<SettingsTabs activeTab="repos" />
8677

8778
{/* Main Content */}
8879
<ReposClient
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import Link from "next/link";
2+
3+
type TabName = "llm-keys" | "platforms" | "repos";
4+
5+
export function SettingsTabs({ activeTab }: { activeTab: TabName }) {
6+
return (
7+
<div className="flex gap-1 rounded-xl bg-zinc-100 p-1">
8+
<Tab href="/settings/llm-keys" label="LLM Keys" isActive={activeTab === "llm-keys"} />
9+
<Tab href="/settings/platforms" label="Platforms" isActive={activeTab === "platforms"} />
10+
<Tab href="/settings/repos" label="Repos" isActive={activeTab === "repos"} />
11+
</div>
12+
);
13+
}
14+
15+
function Tab({ href, label, isActive }: { href: string; label: string; isActive: boolean }) {
16+
if (isActive) {
17+
return (
18+
<span className="flex-1 rounded-lg bg-white px-4 py-2 text-center text-sm font-medium text-zinc-900 shadow-sm">
19+
{label}
20+
</span>
21+
);
22+
}
23+
return (
24+
<Link
25+
href={href}
26+
className="flex-1 rounded-lg px-4 py-2 text-center text-sm font-medium text-zinc-600 transition-colors hover:text-zinc-900"
27+
>
28+
{label}
29+
</Link>
30+
);
31+
}

packages/core/src/__tests__/retry.test.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it, vi } from "vitest";
2-
import { withRetry, isRetryableError } from "../platforms/retry";
3-
import { RateLimitExceededError, TokenExpiredError } from "../platforms/errors";
2+
import { withRetry } from "../platforms/retry";
3+
import { isRetryableError, RateLimitExceededError, TokenExpiredError } from "../platforms/errors";
44

55
describe("withRetry", () => {
66
it("returns result on first successful call", async () => {
@@ -15,8 +15,8 @@ describe("withRetry", () => {
1515
it("retries on failure and succeeds", async () => {
1616
const operation = vi
1717
.fn()
18-
.mockRejectedValueOnce(new Error("fail 1"))
19-
.mockRejectedValueOnce(new Error("fail 2"))
18+
.mockRejectedValueOnce(new Error("network fail 1"))
19+
.mockRejectedValueOnce(new Error("network fail 2"))
2020
.mockResolvedValue("success after retries");
2121

2222
const result = await withRetry(operation, {
@@ -29,11 +29,11 @@ describe("withRetry", () => {
2929
});
3030

3131
it("throws after max retries exhausted", async () => {
32-
const operation = vi.fn().mockRejectedValue(new Error("always fails"));
32+
const operation = vi.fn().mockRejectedValue(new Error("network always fails"));
3333

3434
await expect(
3535
withRetry(operation, { maxRetries: 2, initialDelayMs: 10 })
36-
).rejects.toThrow("always fails");
36+
).rejects.toThrow("network always fails");
3737

3838
expect(operation).toHaveBeenCalledTimes(3); // initial + 2 retries
3939
});
@@ -42,8 +42,8 @@ describe("withRetry", () => {
4242
const onRetry = vi.fn();
4343
const operation = vi
4444
.fn()
45-
.mockRejectedValueOnce(new Error("first error"))
46-
.mockRejectedValueOnce(new Error("second error"))
45+
.mockRejectedValueOnce(new Error("network first error"))
46+
.mockRejectedValueOnce(new Error("network second error"))
4747
.mockResolvedValue("done");
4848

4949
await withRetry(operation, {
@@ -62,9 +62,9 @@ describe("withRetry", () => {
6262
const onRetry = vi.fn((_err, _attempt, delay) => delays.push(delay));
6363
const operation = vi
6464
.fn()
65-
.mockRejectedValueOnce(new Error("1"))
66-
.mockRejectedValueOnce(new Error("2"))
67-
.mockRejectedValueOnce(new Error("3"))
65+
.mockRejectedValueOnce(new Error("network 1"))
66+
.mockRejectedValueOnce(new Error("network 2"))
67+
.mockRejectedValueOnce(new Error("network 3"))
6868
.mockResolvedValue("done");
6969

7070
await withRetry(operation, {
@@ -87,10 +87,10 @@ describe("withRetry", () => {
8787
const onRetry = vi.fn((_err, _attempt, delay) => delays.push(delay));
8888
const operation = vi
8989
.fn()
90-
.mockRejectedValueOnce(new Error("1"))
91-
.mockRejectedValueOnce(new Error("2"))
92-
.mockRejectedValueOnce(new Error("3"))
93-
.mockRejectedValueOnce(new Error("4"))
90+
.mockRejectedValueOnce(new Error("network 1"))
91+
.mockRejectedValueOnce(new Error("network 2"))
92+
.mockRejectedValueOnce(new Error("network 3"))
93+
.mockRejectedValueOnce(new Error("network 4"))
9494
.mockResolvedValue("done");
9595

9696
await withRetry(operation, {

0 commit comments

Comments
 (0)