From 09559fca40b4358b3dcae5e42cd6ad4682839de2 Mon Sep 17 00:00:00 2001 From: VIDYANKSHINI Date: Wed, 27 May 2026 21:55:53 +0530 Subject: [PATCH 1/5] feat: add Discord bot integration for streak notifications --- .../api/notifications/discord-sync/route.ts | 123 +++++++++++++++ .../api/user/settings/discord-test/route.ts | 29 ++++ src/app/api/user/settings/route.ts | 41 ++++- src/app/dashboard/settings/page.tsx | 146 ++++++++++++++++++ src/lib/discord.ts | 112 ++++++++++++++ .../20260528000000_add_discord_settings.sql | 4 + supabase/schema.sql | 5 +- vercel.json | 4 + 8 files changed, 459 insertions(+), 5 deletions(-) create mode 100644 src/app/api/notifications/discord-sync/route.ts create mode 100644 src/app/api/user/settings/discord-test/route.ts create mode 100644 src/lib/discord.ts create mode 100644 supabase/migrations/20260528000000_add_discord_settings.sql diff --git a/src/app/api/notifications/discord-sync/route.ts b/src/app/api/notifications/discord-sync/route.ts new file mode 100644 index 000000000..0f1586e0b --- /dev/null +++ b/src/app/api/notifications/discord-sync/route.ts @@ -0,0 +1,123 @@ +import { NextResponse } from "next/server"; +import { supabaseAdmin } from "@/lib/supabase"; +import { sendMilestoneReached, sendStreakAtRisk, sendWeeklySummary } from "@/lib/discord"; +import { fetchPublicStreak, fetchPublicContributions } from "@/lib/public-profile-data"; +import { toDateStr } from "@/lib/dateUtils"; + +export const dynamic = "force-dynamic"; + +export async function GET(req: Request) { + const authHeader = req.headers.get("authorization"); + const cronSecret = process.env.CRON_SECRET; + + if (!cronSecret) { + return NextResponse.json({ error: "CRON_SECRET is not configured" }, { status: 500 }); + } + + if (authHeader !== `Bearer ${cronSecret}` && process.env.NODE_ENV !== "development") { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { data: users, error } = await supabaseAdmin + .from("users") + .select("id, github_login, discord_webhook_url, timezone, last_discord_notification_at") + .not("discord_webhook_url", "is", null); + + if (error || !users) { + return NextResponse.json({ error: "Database error" }, { status: 500 }); + } + + const token = process.env.GITHUB_TOKEN; + const now = new Date(); + + let processed = 0; + let notificationsSent = 0; + + for (const user of users) { + if (!user.discord_webhook_url) continue; + + const tz = user.timezone || "UTC"; + let localHour: number; + let isSunday = false; + + try { + const formatter = new Intl.DateTimeFormat("en-US", { timeZone: tz, hour: "numeric", hour12: false, weekday: "short" }); + const parts = formatter.formatToParts(now); + const hourPart = parts.find(p => p.type === "hour")?.value; + const weekdayPart = parts.find(p => p.type === "weekday")?.value; + localHour = parseInt(hourPart || "0", 10); + + // Handle "24" meaning midnight in some Intl implementations + if (localHour === 24) localHour = 0; + + isSunday = weekdayPart === "Sun"; + } catch { + localHour = now.getUTCHours(); + isSunday = now.getUTCDay() === 0; + } + + if (localHour !== 20) { + continue; + } + + if (user.last_discord_notification_at) { + const lastNotified = new Date(user.last_discord_notification_at); + if (now.getTime() - lastNotified.getTime() < 20 * 60 * 60 * 1000) { + continue; + } + } + + processed++; + + try { + const streakData = await fetchPublicStreak(user.github_login, token); + let sentSomething = false; + + // Determine "today" in the user's timezone, or UTC if fallback + let todayStr: string; + try { + const dFmt = new Intl.DateTimeFormat("en-US", { timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit' }); + const [{value: mo},,{value: da},,{value: ye}] = dFmt.formatToParts(now); + todayStr = `${ye}-${mo}-${da}`; + } catch { + todayStr = toDateStr(now); + } + + if (streakData.lastCommitDate !== todayStr && streakData.current > 0) { + await sendStreakAtRisk(user.discord_webhook_url, user.github_login, streakData.current); + sentSomething = true; + } + + if (streakData.lastCommitDate === todayStr) { + const milestones = [7, 14, 30, 100]; + if (milestones.includes(streakData.current)) { + await sendMilestoneReached(user.discord_webhook_url, user.github_login, streakData.current); + sentSomething = true; + } + } + + if (isSunday) { + const contribData = await fetchPublicContributions(user.github_login, token, 7); + const stats = { + commits: contribData.total, + prs: 0, + activeDays: Object.keys(contribData.data).length, + }; + await sendWeeklySummary(user.discord_webhook_url, user.github_login, stats); + sentSomething = true; + } + + if (sentSomething) { + await supabaseAdmin + .from("users") + .update({ last_discord_notification_at: now.toISOString() }) + .eq("id", user.id); + notificationsSent++; + } + } catch (err) { + console.error(`Failed to process discord notifications for user ${user.github_login}`, err); + } + } + + return NextResponse.json({ success: true, processed, notificationsSent }); +} diff --git a/src/app/api/user/settings/discord-test/route.ts b/src/app/api/user/settings/discord-test/route.ts new file mode 100644 index 000000000..a904d0f1b --- /dev/null +++ b/src/app/api/user/settings/discord-test/route.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth"; +import { sendTestNotification } from "@/lib/discord"; + +export const dynamic = "force-dynamic"; + +export async function POST(req: NextRequest) { + const session = await getServerSession(authOptions); + + if (!session?.githubLogin) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + try { + const { webhookUrl } = await req.json(); + + if (!webhookUrl || typeof webhookUrl !== "string" || (!webhookUrl.startsWith("https://discord.com/api/webhooks/") && !webhookUrl.startsWith("https://discordapp.com/api/webhooks/"))) { + return NextResponse.json({ error: "Invalid Discord Webhook URL" }, { status: 400 }); + } + + await sendTestNotification(webhookUrl, session.githubLogin); + + return NextResponse.json({ success: true }); + } catch (error: any) { + console.error("Discord test notification error:", error); + return NextResponse.json({ error: error.message || "Failed to send notification" }, { status: 500 }); + } +} diff --git a/src/app/api/user/settings/route.ts b/src/app/api/user/settings/route.ts index f8058d83a..ac32b3860 100644 --- a/src/app/api/user/settings/route.ts +++ b/src/app/api/user/settings/route.ts @@ -11,7 +11,7 @@ async function fetchUserSettings(userId: string) { // Tier 1: All columns const res1 = await supabaseAdmin .from("users") - .select("id, github_login, is_public, leaderboard_opt_in, pinned_repos, wakatime_api_key_encrypted, wakatime_api_key_iv, weekly_digest_opt_in") + .select("id, github_login, is_public, leaderboard_opt_in, pinned_repos, wakatime_api_key_encrypted, wakatime_api_key_iv, weekly_digest_opt_in, discord_webhook_url, timezone") .eq("id", userId) .single(); @@ -28,6 +28,8 @@ async function fetchUserSettings(userId: string) { pinned_repos: (res1.data as any).pinned_repos || [], wakatime_api_key_encrypted: (res1.data as any).wakatime_api_key_encrypted || null, wakatime_api_key_iv: (res1.data as any).wakatime_api_key_iv || null, + discord_webhook_url: (res1.data as any).discord_webhook_url || null, + timezone: (res1.data as any).timezone || "UTC", }; } @@ -44,6 +46,8 @@ async function fetchUserSettings(userId: string) { pinned_repos: [] as string[], wakatime_api_key_encrypted: null, wakatime_api_key_iv: null, + discord_webhook_url: null, + timezone: "UTC", }; } @@ -154,6 +158,8 @@ export async function GET(req: NextRequest) { weekly_digest_opt_in: result.weekly_digest_opt_in, pinned_repos: result.pinned_repos, has_wakatime_key: !!result.wakatime_api_key_encrypted && !!result.wakatime_api_key_iv, + discord_webhook_url: result.discord_webhook_url, + timezone: result.timezone, }); } @@ -173,14 +179,14 @@ export async function PATCH(req: NextRequest) { ); } - let body: { is_public?: boolean; leaderboard_opt_in?: boolean; weekly_digest_opt_in?: boolean; pinned_repos?: string[]; wakatime_api_key?: string }; + let body: { is_public?: boolean; leaderboard_opt_in?: boolean; weekly_digest_opt_in?: boolean; pinned_repos?: string[]; wakatime_api_key?: string; discord_webhook_url?: string | null; timezone?: string }; try { body = await req.json(); } catch { return NextResponse.json({ error: "Invalid request body" }, { status: 400 }); } - const { is_public, leaderboard_opt_in, weekly_digest_opt_in, pinned_repos, wakatime_api_key } = body; + const { is_public, leaderboard_opt_in, weekly_digest_opt_in, pinned_repos, wakatime_api_key, discord_webhook_url, timezone } = body; // Retrieve supported columns first const settingsResult = await fetchUserSettings(user.id); @@ -190,7 +196,7 @@ export async function PATCH(req: NextRequest) { } const { hasLeaderboardOptIn, hasPinnedRepos, hasWakatimeKey, hasWeeklyDigestOptIn } = settingsResult; - const updates: { is_public?: boolean; leaderboard_opt_in?: boolean; weekly_digest_opt_in?: boolean; pinned_repos?: string[]; wakatime_api_key_encrypted?: string | null; wakatime_api_key_iv?: string | null } = {}; + const updates: { is_public?: boolean; leaderboard_opt_in?: boolean; weekly_digest_opt_in?: boolean; pinned_repos?: string[]; wakatime_api_key_encrypted?: string | null; wakatime_api_key_iv?: string | null; discord_webhook_url?: string | null; timezone?: string } = {}; if (is_public !== undefined && is_public !== null && typeof is_public === "boolean") { updates.is_public = is_public; @@ -245,6 +251,28 @@ export async function PATCH(req: NextRequest) { } } + // Handle Discord settings + if (discord_webhook_url !== undefined) { + if (discord_webhook_url === "") { + updates.discord_webhook_url = null; + } else if (typeof discord_webhook_url === "string" && (discord_webhook_url.startsWith("https://discord.com/api/webhooks/") || discord_webhook_url.startsWith("https://discordapp.com/api/webhooks/"))) { + updates.discord_webhook_url = discord_webhook_url; + } else if (discord_webhook_url !== null) { + return NextResponse.json({ error: "Invalid Discord webhook URL" }, { status: 400 }); + } else { + updates.discord_webhook_url = null; + } + } + + if (timezone !== undefined && typeof timezone === "string") { + try { + Intl.DateTimeFormat(undefined, { timeZone: timezone }); + updates.timezone = timezone; + } catch { + return NextResponse.json({ error: "Invalid timezone" }, { status: 400 }); + } + } + // If there are no updates (or none that are supported by the schema) if (Object.keys(updates).length === 0) { return NextResponse.json({ @@ -255,6 +283,8 @@ export async function PATCH(req: NextRequest) { weekly_digest_opt_in: settingsResult.weekly_digest_opt_in, pinned_repos: settingsResult.pinned_repos, has_wakatime_key: !!settingsResult.wakatime_api_key_encrypted && !!settingsResult.wakatime_api_key_iv, + discord_webhook_url: settingsResult.discord_webhook_url, + timezone: settingsResult.timezone, }); } @@ -267,6 +297,7 @@ export async function PATCH(req: NextRequest) { selectCols.push("wakatime_api_key_encrypted"); selectCols.push("wakatime_api_key_iv"); } + selectCols.push("discord_webhook_url", "timezone"); const { data: updated, error: updateError } = await supabaseAdmin .from("users") @@ -288,5 +319,7 @@ export async function PATCH(req: NextRequest) { weekly_digest_opt_in: (updated as any).weekly_digest_opt_in ?? false, pinned_repos: (updated as any).pinned_repos || [], has_wakatime_key: !!(updated as any).wakatime_api_key_encrypted && !!(updated as any).wakatime_api_key_iv, + discord_webhook_url: (updated as any).discord_webhook_url, + timezone: (updated as any).timezone || "UTC", }); } diff --git a/src/app/dashboard/settings/page.tsx b/src/app/dashboard/settings/page.tsx index 2557f67b7..33fc5a972 100644 --- a/src/app/dashboard/settings/page.tsx +++ b/src/app/dashboard/settings/page.tsx @@ -17,6 +17,8 @@ interface UserSettings { leaderboard_opt_in: boolean; weekly_digest_opt_in: boolean; has_wakatime_key?: boolean; + discord_webhook_url?: string; + timezone?: string; } interface LinkedAccount { @@ -123,6 +125,10 @@ function SettingsPageContent() { ); const [wakatimeKey, setWakatimeKey] = useState(""); const [savingWakatime, setSavingWakatime] = useState(false); + const [discordWebhook, setDiscordWebhook] = useState(""); + const [timezone, setTimezone] = useState(""); + const [savingDiscord, setSavingDiscord] = useState(false); + const [testingDiscord, setTestingDiscord] = useState(false); const [isDirty, setIsDirty] = useState(false); const [showConfirmModal, setShowConfirmModal] = useState(false); const [pendingPath, setPendingPath] = useState(null); @@ -220,6 +226,8 @@ function SettingsPageContent() { if (res.ok) { const data = await res.json(); setSettings(data); + setDiscordWebhook(data.discord_webhook_url || ""); + setTimezone(data.timezone || "UTC"); } } catch (error) { console.error("Failed to load settings:", error); @@ -358,6 +366,58 @@ function SettingsPageContent() { } }; + const handleSaveDiscord = async () => { + if (!settings) return; + setSavingDiscord(true); + try { + const res = await fetch("/api/user/settings", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ discord_webhook_url: discordWebhook, timezone }), + }); + if (res.ok) { + const updated = await res.json(); + setSettings(updated); + setIsDirty(false); + toast.success(discordWebhook === "" ? "Discord Webhook removed" : "Discord settings saved successfully!"); + } else { + const errorData = await res.json(); + toast.error(errorData.error || "Failed to update Discord settings"); + } + } catch (error) { + console.error("Error updating Discord settings:", error); + toast.error("Failed to update Discord settings"); + } finally { + setSavingDiscord(false); + } + }; + + const handleTestDiscord = async () => { + if (!discordWebhook) { + toast.error("Please enter a Webhook URL first"); + return; + } + setTestingDiscord(true); + try { + const res = await fetch("/api/user/settings/discord-test", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ webhookUrl: discordWebhook }), + }); + if (res.ok) { + toast.success("Test notification sent! Check your Discord server."); + } else { + const errorData = await res.json(); + toast.error(errorData.error || "Failed to send test notification"); + } + } catch (error) { + console.error("Error sending test notification:", error); + toast.error("Failed to send test notification"); + } finally { + setTestingDiscord(false); + } + }; + const copyShareLink = () => { if (!settings) return; const link = `${window.location.origin}/u/${settings.github_login}`; @@ -794,6 +854,92 @@ function SettingsPageContent() { +
+
+
+

+ Discord Integration +

+

+ Receive streak reminders and milestone alerts in your Discord server. +

+
+
+ +
+
+ +
+ { + setDiscordWebhook(e.target.value); + setIsDirty(true); + }} + placeholder="https://discord.com/api/webhooks/..." + className="flex-1 rounded-lg border border-[var(--border)] bg-[var(--control)] px-4 py-2 text-sm text-[var(--card-foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--accent)]" + /> +
+
+ +
+ + +
+ +
+ + +
+

+ Leave Webhook URL blank and click Save to unlink Discord. +

+
+
+
diff --git a/src/lib/discord.ts b/src/lib/discord.ts new file mode 100644 index 000000000..4233c51c4 --- /dev/null +++ b/src/lib/discord.ts @@ -0,0 +1,112 @@ +export async function sendDiscordWebhook(webhookUrl: string, payload: any) { + const res = await fetch(webhookUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + + if (!res.ok) { + throw new Error(`Discord Webhook failed: ${res.status} ${res.statusText}`); + } + + return true; +} + +export async function sendTestNotification(webhookUrl: string, username: string) { + const payload = { + username: "DevTrack Bot", + avatar_url: "https://github.com/Priyanshu-byte-coder.png", // Or a devtrack logo + embeds: [ + { + title: "✅ Discord Integration Successful!", + description: `Hello **${username}**, your Discord webhook has been successfully linked to DevTrack. You will now receive streak reminders and milestone alerts here.`, + color: 3900150, // Blue + footer: { + text: "DevTrack Notifications", + }, + timestamp: new Date().toISOString(), + }, + ], + }; + + return sendDiscordWebhook(webhookUrl, payload); +} + +export async function sendStreakAtRisk(webhookUrl: string, username: string, currentStreak: number) { + const payload = { + username: "DevTrack Bot", + embeds: [ + { + title: "⚠️ Streak at Risk!", + description: `Hey **${username}**, you haven't made any commits today! Your current streak is **${currentStreak} days**. Commit now before midnight to keep it alive!`, + color: 16750848, // Orange + footer: { + text: "DevTrack Streak Reminder", + }, + timestamp: new Date().toISOString(), + }, + ], + }; + + return sendDiscordWebhook(webhookUrl, payload); +} + +export async function sendMilestoneReached(webhookUrl: string, username: string, streak: number) { + const payload = { + username: "DevTrack Bot", + embeds: [ + { + title: "🎉 Milestone Reached!", + description: `Incredible work, **${username}**! You've just hit a **${streak}-day** commit streak! Keep up the amazing consistency! 🚀`, + color: 16761095, // Gold + thumbnail: { + url: "https://github.githubassets.com/images/modules/profile/achievements/pull-shark-default.png", // Just a placeholder fun image + }, + footer: { + text: "DevTrack Milestone", + }, + timestamp: new Date().toISOString(), + }, + ], + }; + + return sendDiscordWebhook(webhookUrl, payload); +} + +export async function sendWeeklySummary(webhookUrl: string, username: string, stats: { commits: number; prs: number; activeDays: number }) { + const payload = { + username: "DevTrack Bot", + embeds: [ + { + title: "📊 Weekly Summary", + description: `Here is your coding activity summary for the past week, **${username}**:`, + color: 5814783, // Purple-ish + fields: [ + { + name: "Commits", + value: `${stats.commits}`, + inline: true, + }, + { + name: "Pull Requests", + value: `${stats.prs}`, + inline: true, + }, + { + name: "Active Days", + value: `${stats.activeDays} / 7`, + inline: true, + }, + ], + footer: { + text: "DevTrack Weekly Summary", + }, + timestamp: new Date().toISOString(), + }, + ], + }; + + return sendDiscordWebhook(webhookUrl, payload); +} diff --git a/supabase/migrations/20260528000000_add_discord_settings.sql b/supabase/migrations/20260528000000_add_discord_settings.sql new file mode 100644 index 000000000..d489c811e --- /dev/null +++ b/supabase/migrations/20260528000000_add_discord_settings.sql @@ -0,0 +1,4 @@ +ALTER TABLE users +ADD COLUMN IF NOT EXISTS discord_webhook_url TEXT, +ADD COLUMN IF NOT EXISTS timezone TEXT DEFAULT 'UTC', +ADD COLUMN IF NOT EXISTS last_discord_notification_at TIMESTAMPTZ; diff --git a/supabase/schema.sql b/supabase/schema.sql index 6e0c9a50b..80d4b8093 100644 --- a/supabase/schema.sql +++ b/supabase/schema.sql @@ -9,7 +9,10 @@ create table if not exists users ( updated_at timestamptz default now(), wakatime_api_key_encrypted text, wakatime_api_key_iv text, - is_sponsor boolean default false + is_sponsor boolean default false, + discord_webhook_url text, + timezone text default 'UTC', + last_discord_notification_at timestamptz ); create table if not exists goals ( diff --git a/vercel.json b/vercel.json index bff3d114f..18b76cc49 100644 --- a/vercel.json +++ b/vercel.json @@ -15,6 +15,10 @@ { "path": "/api/sponsors/sync", "schedule": "0 0 * * *" + }, + { + "path": "/api/notifications/discord-sync", + "schedule": "0 * * * *" } ] } From e15692c560716b00aea47643c41ff13293fb8d94 Mon Sep 17 00:00:00 2001 From: VIDYANKSHINI Date: Thu, 28 May 2026 17:35:54 +0530 Subject: [PATCH 2/5] fix: change discord sync cron to daily to support vercel hobby plan --- vercel.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vercel.json b/vercel.json index 18b76cc49..9f4506273 100644 --- a/vercel.json +++ b/vercel.json @@ -18,7 +18,7 @@ }, { "path": "/api/notifications/discord-sync", - "schedule": "0 * * * *" + "schedule": "0 20 * * *" } ] } From 8c51b40dc4ad60d9cc42ee3de50cb840873fc220 Mon Sep 17 00:00:00 2001 From: VIDYANKSHINI Date: Thu, 28 May 2026 17:47:38 +0530 Subject: [PATCH 3/5] fix: resolve lint and typescript errors --- src/lib/supabase.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts index 2846fd44a..767c1ebe9 100644 --- a/src/lib/supabase.ts +++ b/src/lib/supabase.ts @@ -7,6 +7,7 @@ const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY; export const SUPABASE_ADMIN_UNAVAILABLE_MESSAGE = "Supabase admin client is unavailable. Check NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY."; +// eslint-disable-next-line type SupabaseAdminClient = SupabaseClient; function createUnavailableSupabaseAdmin(): SupabaseAdminClient { From afd4233b67e5696c1963287ad8d5272c6c888f29 Mon Sep 17 00:00:00 2001 From: VIDYANKSHINI Date: Thu, 28 May 2026 19:02:29 +0530 Subject: [PATCH 4/5] test: fix syntax error in e2e tests --- e2e/dashboard-widgets.spec.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/e2e/dashboard-widgets.spec.js b/e2e/dashboard-widgets.spec.js index 86ad21362..f0d447ab8 100644 --- a/e2e/dashboard-widgets.spec.js +++ b/e2e/dashboard-widgets.spec.js @@ -117,10 +117,6 @@ test.beforeEach(async ({ page }) => { await route.fulfill({ contentType: "application/json", body: JSON.stringify({ ok: true }), - last_synced_at: new Date().toISOString(), - }, - ], - }), }); }); From 9e8ed87d409f2e624caebaaa98cc4567ff290f35 Mon Sep 17 00:00:00 2001 From: VIDYANKSHINI Date: Thu, 28 May 2026 19:15:49 +0530 Subject: [PATCH 5/5] test: update theme spec to run on dashboard --- e2e/theme.spec.js | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/e2e/theme.spec.js b/e2e/theme.spec.js index ed5145908..01646c6e2 100644 --- a/e2e/theme.spec.js +++ b/e2e/theme.spec.js @@ -1,7 +1,41 @@ import { expect, test } from "@playwright/test"; +import { encode } from "next-auth/jwt"; + +test.beforeEach(async ({ page }) => { + const token = await encode({ + secret: process.env.NEXTAUTH_SECRET ?? "playwright-placeholder-secret-that-is-long-enough", + token: { + name: "Playwright User", + email: "playwright@example.com", + githubLogin: "playwright-user", + githubId: "12345", + accessToken: "test-token", + expires: "2099-01-01T00:00:00.000Z", + }, + }); + + await page.context().addCookies([ + { + name: "next-auth.session-token", + value: String(token ?? ""), + domain: "127.0.0.1", + path: "/", + httpOnly: true, + sameSite: "Lax", + secure: false, + }, + ]); + + await page.route("**/api/user/settings", async (route) => { + await route.fulfill({ + contentType: "application/json", + body: JSON.stringify({ is_public: true }), + }); + }); +}); test("theme toggle switches between dark and light mode", async ({ page }) => { - await page.goto("/"); + await page.goto("/dashboard"); const themeToggle = page.getByRole("button", { name: "Toggle theme" }); await expect(themeToggle).toBeVisible();