From a2afe1ed86800f4933ebcd447a6aacc4dbff826f Mon Sep 17 00:00:00 2001 From: Melvin Jones Repol Date: Mon, 17 Aug 2026 01:29:19 +0800 Subject: [PATCH 01/10] feat(wakatime): improve auth & wakatime api key onboarding flow --- app/{(public) => }/(auth)/logout/page.tsx | 0 .../(auth)/verify-email/page.tsx | 1 - app/(auth)/verify-wakatime/page.tsx | 33 +++ app/components/auth/VerifyEmail.tsx | 20 +- app/components/auth/VerifyWakatime.tsx | 205 ++++++++++++++++++ .../auth/form/ForgotPasswordForm.tsx | 9 +- app/components/auth/form/LoginForm.tsx | 8 +- .../auth/form/ResetPasswordForm.tsx | 6 +- app/components/auth/form/SignupForm.tsx | 6 +- .../auth/form/UpdatePasswordForm.tsx | 6 +- .../{WithKey.tsx => Leaderboards.tsx} | 2 +- app/components/dashboard/WithoutKey.tsx | 162 -------------- app/d/leaderboards/page.tsx | 4 +- app/d/page.tsx | 4 - app/lib/auth.ts | 8 +- app/lib/proxy/auth.ts | 11 +- next-auth.d.ts | 2 + proxy.ts | 1 + 18 files changed, 277 insertions(+), 211 deletions(-) rename app/{(public) => }/(auth)/logout/page.tsx (100%) rename app/{(public) => }/(auth)/verify-email/page.tsx (89%) create mode 100644 app/(auth)/verify-wakatime/page.tsx create mode 100644 app/components/auth/VerifyWakatime.tsx rename app/components/dashboard/{WithKey.tsx => Leaderboards.tsx} (99%) delete mode 100644 app/components/dashboard/WithoutKey.tsx diff --git a/app/(public)/(auth)/logout/page.tsx b/app/(auth)/logout/page.tsx similarity index 100% rename from app/(public)/(auth)/logout/page.tsx rename to app/(auth)/logout/page.tsx diff --git a/app/(public)/(auth)/verify-email/page.tsx b/app/(auth)/verify-email/page.tsx similarity index 89% rename from app/(public)/(auth)/verify-email/page.tsx rename to app/(auth)/verify-email/page.tsx index 549d5e8..6111665 100644 --- a/app/(public)/(auth)/verify-email/page.tsx +++ b/app/(auth)/verify-email/page.tsx @@ -6,7 +6,6 @@ import { redirect } from "next/navigation"; export const metadata: Metadata = { title: "Verify Email - Devpulse", - description: "Verify your email address to activate your Devpulse account.", }; export default async function VerifyEmailPage() { diff --git a/app/(auth)/verify-wakatime/page.tsx b/app/(auth)/verify-wakatime/page.tsx new file mode 100644 index 0000000..b47f453 --- /dev/null +++ b/app/(auth)/verify-wakatime/page.tsx @@ -0,0 +1,33 @@ +import { Metadata } from "next/types"; +import { Suspense } from "react"; +import { auth } from "@/app/lib/auth"; +import { redirect } from "next/navigation"; +import VerifyWakatime from "@/app/components/auth/VerifyWakatime"; + +export const metadata: Metadata = { + title: "Verify Wakatime - Devpulse", +}; + +export default async function VerifyWakatimePage() { + const session = await auth(); + + if (!session) { + return redirect("/login"); + } + + if (session.user.wakatimeApiKey) { + return redirect("/"); + } + + return ( + + Loading... + + } + > + + + ); +} diff --git a/app/components/auth/VerifyEmail.tsx b/app/components/auth/VerifyEmail.tsx index 9339c79..07d5ec8 100644 --- a/app/components/auth/VerifyEmail.tsx +++ b/app/components/auth/VerifyEmail.tsx @@ -100,7 +100,7 @@ export default function VerifyEmail({

- One step away from your dashboard. + Two steps away from your dashboard.

We sent a verification link to your inbox. Click it to activate your @@ -136,7 +136,7 @@ export default function VerifyEmail({

- {"// Welcome aboard. ✓"} + {"// Next. Wakatime API key. ->"}
@@ -192,16 +192,16 @@ export default function VerifyEmail({ )} - Back to login + Changed your mind? Log in again. ) : ( @@ -248,17 +248,17 @@ export default function VerifyEmail({ )} - Back to login + Changed your mind? Log in again. diff --git a/app/components/auth/VerifyWakatime.tsx b/app/components/auth/VerifyWakatime.tsx new file mode 100644 index 0000000..f9dea4a --- /dev/null +++ b/app/components/auth/VerifyWakatime.tsx @@ -0,0 +1,205 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { toast } from "react-toastify"; + +export default function VerifyWakatime() { + const [apiKey, setApiKey] = useState(""); + const [grecaptchaLoaded, setGrecaptchaLoaded] = useState(false); + const [loading, setLoading] = useState(false); + + useEffect(() => { + const loadGrecaptcha = () => { + const scriptId = "recaptcha-enterprise"; + if (!document.getElementById(scriptId)) { + const script = document.createElement("script"); + script.id = scriptId; + script.src = `https://www.google.com/recaptcha/enterprise.js?render=${process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY}`; + script.async = true; + script.onload = () => setGrecaptchaLoaded(true); + document.body.appendChild(script); + } else { + setGrecaptchaLoaded(true); + } + }; + + loadGrecaptcha(); + }, []); + + const handleVerify = async (e: React.FormEvent) => { + e.preventDefault(); + if (!grecaptchaLoaded || !window.grecaptcha?.enterprise) { + toast.error( + "Recaptcha Enterprise is not loaded. Please try again later.", + ); + return; + } + + setLoading(true); + + const verifyWakatimePromise = new Promise(async (resolve, reject) => { + try { + const wakatimeSyncResponse = await fetch( + `/api/wakatime/sync?apiKey=${encodeURIComponent(apiKey)}`, + ); + if (!wakatimeSyncResponse.ok) + throw new Error("Failed to sync Wakatime."); + + resolve(); + } catch (error) { + reject(error); + } + }); + + toast.promise(verifyWakatimePromise, { + pending: "Verifying...", + success: "Verification successful!", + error: "Failed to verify. Please try again.", + }); + + verifyWakatimePromise.finally(() => { + setLoading(false); + }); + }; + + return ( +
+ {/* Left Side - Visual / Branding */} +
+
+ +
+ + Devpulse Logo + + Devpulse + + +
+ +
+

+ Last step away from your dashboard. +

+

+ We need you to input your Wakatime API key to complete the + verification. +

+ +
+
+
+
+
+ + verify.ts + +
+
+
+ await + user + . + verifyWakatime + ( + apiKey + ); +
+
+ user + . + verifyWakatime + = + true + ; +
+
+ + {"// Welcome aboard. ✓"} + +
+
+
+
+ +
+ © {new Date().getFullYear()} Devpulse. All rights reserved. +
+
+ + {/* Right Side */} +
+
+ +
+ + Devpulse Logo +

Devpulse

+ + +
+
+ + + +
+

+ Wakatime Verification +

+

+ We need you to enter your Wakatime API Key to connect your account + to Devpulse. +

+
+ +
+
+ setApiKey(e.target.value)} + required + /> + + + + + Changed your mind? Log in again. + +
+
+
+
+
+ ); +} diff --git a/app/components/auth/form/ForgotPasswordForm.tsx b/app/components/auth/form/ForgotPasswordForm.tsx index 49e4663..9eceb76 100644 --- a/app/components/auth/form/ForgotPasswordForm.tsx +++ b/app/components/auth/form/ForgotPasswordForm.tsx @@ -94,13 +94,8 @@ export default function ForgotPasswordForm() { diff --git a/app/components/auth/form/LoginForm.tsx b/app/components/auth/form/LoginForm.tsx index 5f42978..abd466d 100644 --- a/app/components/auth/form/LoginForm.tsx +++ b/app/components/auth/form/LoginForm.tsx @@ -44,6 +44,7 @@ export default function LoginForm() { const handleLogin = async (e: React.SyntheticEvent) => { e.preventDefault(); + if (!grecaptchaLoaded || !window.grecaptcha?.enterprise) { toast.error( "Recaptcha Enterprise is not loaded. Please try again later.", @@ -119,15 +120,10 @@ export default function LoginForm() { onChange={(e) => setPassword(e.target.value)} required /> - diff --git a/app/components/auth/form/ResetPasswordForm.tsx b/app/components/auth/form/ResetPasswordForm.tsx index 438e203..8bac0bb 100644 --- a/app/components/auth/form/ResetPasswordForm.tsx +++ b/app/components/auth/form/ResetPasswordForm.tsx @@ -124,11 +124,7 @@ export default function ResetPasswordForm() { diff --git a/app/components/auth/form/SignupForm.tsx b/app/components/auth/form/SignupForm.tsx index 86ab1c1..ee1e48e 100644 --- a/app/components/auth/form/SignupForm.tsx +++ b/app/components/auth/form/SignupForm.tsx @@ -162,11 +162,7 @@ export default function SignupForm() { diff --git a/app/components/auth/form/UpdatePasswordForm.tsx b/app/components/auth/form/UpdatePasswordForm.tsx index 8761d64..9e8cb93 100644 --- a/app/components/auth/form/UpdatePasswordForm.tsx +++ b/app/components/auth/form/UpdatePasswordForm.tsx @@ -107,11 +107,7 @@ export default function UpdatePasswordForm() { diff --git a/app/components/dashboard/WithKey.tsx b/app/components/dashboard/Leaderboards.tsx similarity index 99% rename from app/components/dashboard/WithKey.tsx rename to app/components/dashboard/Leaderboards.tsx index 924cf89..1b34c1a 100644 --- a/app/components/dashboard/WithKey.tsx +++ b/app/components/dashboard/Leaderboards.tsx @@ -14,7 +14,7 @@ import { sanitizeTextWithBlocklist } from "@/app/utils/moderation"; type ModalState = "create" | "join" | "share" | null; -export default function DashboardWithKey() { +export default function Leaderboards() { const [leaderboardName, setLeaderboardName] = useState(""); const [joinCode, setJoinCode] = useState(""); const [activeModal, setActiveModal] = useState(null); diff --git a/app/components/dashboard/WithoutKey.tsx b/app/components/dashboard/WithoutKey.tsx deleted file mode 100644 index 99fb09f..0000000 --- a/app/components/dashboard/WithoutKey.tsx +++ /dev/null @@ -1,162 +0,0 @@ -"use client"; - -import { useState } from "react"; -import Link from "next/link"; -import { toast } from "react-toastify"; - -export default function DashboardWithoutKey({ email }: { email: string }) { - const [key, setKey] = useState(""); - const [loading, setLoading] = useState(false); - - const WAKATIME_KEY_REGEX = /^waka_[0-9a-f-]{36}$/i; - - const testApiKey = async (apiKey: string) => { - const response = await fetch( - `/api/wakatime/sync?apiKey=${encodeURIComponent(apiKey)}`, - ); - return response.ok; - }; - - const saveKey = async () => { - const nextKey = key.trim(); - - if (!nextKey || !WAKATIME_KEY_REGEX.test(nextKey)) { - toast.error("Please enter a valid WakaTime API key."); - return; - } - - setLoading(true); - - const keyRequest = new Promise(async (resolve, reject) => { - try { - const isValid = await testApiKey(nextKey); - if (!isValid) - return reject( - new Error("Invalid API key. Please check and try again."), - ); - - resolve(); - } catch (error) { - reject(error); - } - }); - - toast.promise(keyRequest, { - pending: "Please wait while we validate and save your API key...", - success: "API key saved! Redirecting...", - error: { - render({ data }) { - setLoading(false); - const err = data as Error; - return err?.message || "Failed to login. Please try again."; - }, - }, - }); - - keyRequest.then(() => { - window.location.reload(); - }); - }; - - return ( -
-
-
-
-

- Account Setup -

-

- Complete your account -

-

- Connect WakaTime to unlock your dashboard insights, rankings, and - coding activity trends. -

-
- - - Step 1 of 1 - -
-
- -
-
-
-

- Account Email -

-

{email}

-
- - - setKey(e.target.value)} - disabled={loading} - /> - - - -

- Get your API key from{" "} - - WakaTime account settings - - . -

-
- -
-

- What you unlock -

- -
-

Daily and weekly coding performance charts.

-

Language, editor, machine, and category insights.

-

Leaderboard participation and progress tracking.

-
- -
-

- Security Note -

-

- Your key is used only to sync your coding data and can be updated - anytime in settings. -

-
-
-
- -

- New account setup is almost done. Connect WakaTime to finish onboarding - and start using your full dashboard. -

-
- ); -} diff --git a/app/d/leaderboards/page.tsx b/app/d/leaderboards/page.tsx index a73c370..0158ed4 100644 --- a/app/d/leaderboards/page.tsx +++ b/app/d/leaderboards/page.tsx @@ -1,4 +1,4 @@ -import DashboardWithKey from "@/app/components/dashboard/WithKey"; +import Leaderboards from "@/app/components/dashboard/Leaderboards"; import LeaderboardsList from "@/app/components/dashboard/LeaderbordList"; import { getCurrentUser } from "@/app/lib/auth/user"; import { Metadata } from "next/types"; @@ -23,7 +23,7 @@ export default async function LeaderboardsPage() { Create, join, and manage your coding servers

- +
diff --git a/app/d/page.tsx b/app/d/page.tsx index 00a0689..dba7e6a 100644 --- a/app/d/page.tsx +++ b/app/d/page.tsx @@ -1,5 +1,4 @@ import { redirect } from "next/navigation"; -import DashboardWithoutKey from "../components/dashboard/WithoutKey"; import Stats from "@/app/components/dashboard/Stats"; import { Metadata } from "next/types"; import { getCurrentUser } from "@/app/lib/auth/user"; @@ -12,8 +11,5 @@ export default async function Dashboard() { const { user } = await getCurrentUser(); if (!user) redirect("/login"); - if (!user.wakatimeApiKey) { - return ; - } return ; } diff --git a/app/lib/auth.ts b/app/lib/auth.ts index 12b05df..dfab02b 100644 --- a/app/lib/auth.ts +++ b/app/lib/auth.ts @@ -72,15 +72,18 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ // forced re-login. if ( token.sub && - (typeof token.role !== "string" || !token.emailVerified) + (typeof token.role !== "string" || + !token.emailVerified || + !token.wakatimeApiKey) ) { const dbUser = await prisma.user.findUnique({ where: { id: token.sub }, - select: { role: true, emailVerified: true }, + select: { role: true, emailVerified: true, wakatimeApiKey: true }, }); if (dbUser) { token.role = dbUser.role; token.emailVerified = dbUser.emailVerified; + token.wakatimeApiKey = dbUser.wakatimeApiKey; } } @@ -93,6 +96,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ session.user.role = typeof token.role === "string" ? token.role : "user"; session.user.emailVerified = (token.emailVerified as Date | null | undefined) ?? null; + session.user.wakatimeApiKey = token.wakatimeApiKey; return session; }, async signIn({ user, account }) { diff --git a/app/lib/proxy/auth.ts b/app/lib/proxy/auth.ts index b5ab9df..f4573ca 100644 --- a/app/lib/proxy/auth.ts +++ b/app/lib/proxy/auth.ts @@ -17,6 +17,9 @@ export default async function Auth(req: NextRequest) { if (!session.user.emailVerified) { return NextResponse.redirect(new URL("/verify-email", req.url)); } + if (!session.user.wakatimeApiKey && pathname.startsWith("/d")) { + return NextResponse.redirect(new URL("/verify-wakatime", req.url)); + } } const authRoutes = [ @@ -30,10 +33,16 @@ export default async function Auth(req: NextRequest) { if (!session.user.emailVerified) { return NextResponse.redirect(new URL("/verify-email", req.url)); } + if (!session.user.wakatimeApiKey) { + return NextResponse.redirect(new URL("/verify-wakatime", req.url)); + } return NextResponse.redirect(new URL("/d", req.url)); } - if (pathname === "/verify-email" && session?.user.emailVerified) { + if ( + (pathname === "/verify-email" && session?.user.emailVerified) || + (pathname === "/verify-wakatime" && session?.user.wakatimeApiKey) + ) { return NextResponse.redirect(new URL("/d", req.url)); } diff --git a/next-auth.d.ts b/next-auth.d.ts index c77bb3e..abb91d9 100644 --- a/next-auth.d.ts +++ b/next-auth.d.ts @@ -10,6 +10,7 @@ declare module "next-auth" { email?: string | null; name?: string | null; image?: string | null; + wakatimeApiKey?: string | null; }; } @@ -24,5 +25,6 @@ declare module "next-auth/jwt" { id?: string; role?: string; emailVerified?: Date | null; + wakatimeApiKey?: string | null; } } diff --git a/proxy.ts b/proxy.ts index a09e206..c7518f8 100644 --- a/proxy.ts +++ b/proxy.ts @@ -35,6 +35,7 @@ export const config = { "/forgot-password", "/reset-password", "/verify-email", + "/verify-wakatime", "/logout", ], }; From 06f2b2ab6dc5ffb154cfdc169d36863e940d28df Mon Sep 17 00:00:00 2001 From: Melvin Jones Repol Date: Mon, 17 Aug 2026 01:45:41 +0800 Subject: [PATCH 02/10] fix(wakatime): syncronize issue due to bigint and setup --- app/components/auth/VerifyWakatime.tsx | 16 +++++++++++++--- app/lib/wakatime/sync.ts | 16 ++++++++++++++-- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/app/components/auth/VerifyWakatime.tsx b/app/components/auth/VerifyWakatime.tsx index f9dea4a..023fde6 100644 --- a/app/components/auth/VerifyWakatime.tsx +++ b/app/components/auth/VerifyWakatime.tsx @@ -2,6 +2,7 @@ import Image from "next/image"; import Link from "next/link"; +import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { toast } from "react-toastify"; @@ -9,6 +10,7 @@ export default function VerifyWakatime() { const [apiKey, setApiKey] = useState(""); const [grecaptchaLoaded, setGrecaptchaLoaded] = useState(false); const [loading, setLoading] = useState(false); + const router = useRouter(); useEffect(() => { const loadGrecaptcha = () => { @@ -59,9 +61,17 @@ export default function VerifyWakatime() { error: "Failed to verify. Please try again.", }); - verifyWakatimePromise.finally(() => { - setLoading(false); - }); + verifyWakatimePromise + .then(() => { + router.push("/d"); + }) + .catch(() => { + // already surfaced via toast.promise + // avoid unhandled rejection + }) + .finally(() => { + setLoading(false); + }); }; return ( diff --git a/app/lib/wakatime/sync.ts b/app/lib/wakatime/sync.ts index 91da57b..7d1243f 100644 --- a/app/lib/wakatime/sync.ts +++ b/app/lib/wakatime/sync.ts @@ -143,6 +143,14 @@ export async function saveWakatimeApiKey({ } } +function serializeBigInts(value: T): T { + return JSON.parse( + JSON.stringify(value, (_key, val) => + typeof val === "bigint" ? val.toString() : val, + ), + ); +} + /** * Fetches WakaTime data and upserts stats, projects, and a daily snapshot. * Skips the remote fetch if data is fresh (< 6 hours old) and the key hasn't changed. @@ -171,7 +179,11 @@ export async function syncWakatimeData({ Date.now() - lastFetch < SIX_HOURS_MS && (existingDailyStats as unknown[]).length >= CONSISTENCY_DAYS ) { - return { status: 200, success: true, data: existing }; + return { + status: 200, + success: true, + data: serializeBigInts(existing), + }; } } } @@ -273,6 +285,6 @@ export async function syncWakatimeData({ return { status: 200, success: true, - data: mergedResult, + data: serializeBigInts(mergedResult), }; } From c4bf9d79677a3457d564dadceea0c12d9c9c31a1 Mon Sep 17 00:00:00 2001 From: Melvin Jones Repol Date: Sun, 23 Aug 2026 05:18:17 +0800 Subject: [PATCH 03/10] fix(devpulse): multiple bug fixes and improvements --- app/(auth)/verify-wakatime/page.tsx | 2 +- app/(public)/(auth)/reset-password/page.tsx | 2 +- app/(public)/flex/page.tsx | 4 +- app/(public)/join/page.tsx | 26 +- app/(public)/leaderboard/[slug]/page.tsx | 2 +- app/(public)/leaderboard/page.tsx | 10 +- app/api/admin/stats/route.ts | 6 +- app/api/auth/forgot-password/route.ts | 2 +- app/api/auth/reset-password/route.ts | 4 +- app/api/auth/verify-email/route.ts | 4 +- app/api/conversations/[id]/presence/route.ts | 2 +- app/api/conversations/[id]/route.ts | 2 +- app/api/conversations/[id]/unread/route.ts | 6 +- app/api/conversations/route.ts | 10 +- app/api/cron/cleanup-messages/route.ts | 2 +- app/api/flex/[id]/route.ts | 16 +- app/api/flex/projects/route.ts | 2 +- app/api/flex/route.ts | 18 +- app/api/kanban/projects/route.ts | 2 +- app/api/leaderboards/[id]/leave/route.ts | 2 +- app/api/leaderboards/join/route.ts | 4 +- app/api/leaderboards/route.ts | 4 +- app/api/messages/route.ts | 12 +- app/api/presence/route.ts | 2 +- app/api/sse/chat/[conversationId]/route.ts | 2 +- .../sse/chat/[conversationId]/typing/route.ts | 2 +- app/api/users/badges/route.ts | 4 +- app/api/users/route.ts | 4 +- app/api/wakatime/sync/route.ts | 2 +- app/components/BoardList.tsx | 18 +- app/components/Chat.tsx | 62 ++-- app/components/Flex.tsx | 20 +- app/components/JoinButton.tsx | 8 +- app/components/admin/Dashbord.tsx | 2 +- app/components/auth/ForgotPassword.tsx | 4 +- app/components/auth/Login.tsx | 8 +- app/components/auth/Logout.tsx | 2 +- app/components/auth/Oauth2.tsx | 6 +- app/components/auth/Signup.tsx | 4 +- app/components/auth/VerifyEmail.tsx | 6 +- app/components/auth/VerifyWakatime.tsx | 4 +- app/components/chat/Conversations.tsx | 6 +- app/components/chat/Messages.tsx | 14 +- app/components/chat/Player.tsx | 8 +- .../chat/hooks/useActiveConversationStream.ts | 6 +- app/components/chat/hooks/useChatBadges.ts | 2 +- .../chat/hooks/useChatConversationActions.ts | 6 +- .../hooks/useChatConversationsRealtime.ts | 6 +- .../chat/hooks/useChatMessageComposer.ts | 2 +- app/components/chat/hooks/useChatPresence.ts | 2 +- app/components/chat/hooks/useChatTyping.ts | 2 +- .../chat/hooks/useChatUserPicker.ts | 2 +- app/components/common/NavProfileDropdown.tsx | 2 +- app/components/common/ui/CTA.tsx | 4 +- app/components/dashboard/Leaderboards.tsx | 8 +- app/components/dashboard/LeaderbordList.tsx | 8 +- app/components/dashboard/Navbar.tsx | 4 +- app/components/dashboard/Settings/Profile.tsx | 4 +- .../dashboard/Settings/WakaTimeKey.tsx | 2 +- app/components/dashboard/Stats.tsx | 247 +++---------- .../dashboard/widgets/Categories.tsx | 10 +- .../dashboard/widgets/CodingActivity.tsx | 25 +- .../widgets/CodingConsistencyHeatmap.tsx | 26 +- .../dashboard/widgets/Dependencies.tsx | 10 +- app/components/dashboard/widgets/Editors.tsx | 13 +- .../widgets/LanguageDestribution.tsx | 16 +- app/components/dashboard/widgets/Machines.tsx | 10 +- .../dashboard/widgets/OperatingSystem.tsx | 12 +- app/components/dashboard/widgets/Projects.tsx | 92 +++-- .../dashboard/widgets/StatsCard.tsx | 36 +- .../landing-page/ContributeCard.tsx | 2 +- app/components/landing-page/Contributors.tsx | 6 +- app/components/landing-page/LosserMembers.tsx | 16 +- .../landing-page/RecentLeaderboard.tsx | 32 +- app/components/landing-page/TopLeaderbord.tsx | 8 +- app/components/landing-page/VibeCoders.tsx | 16 +- app/components/layout/Nav.tsx | 2 +- app/components/leaderboard/BackButton.tsx | 4 +- .../leaderboard/InviteFriendsButton.tsx | 2 +- .../leaderboard/LeaderboardStats.tsx | 2 +- .../leaderboard/LeaderboardTable.tsx | 4 +- app/d/kanban/page.tsx | 22 +- app/d/settings/page.tsx | 8 +- app/internal-server-error.tsx | 6 +- app/lib/auth.ts | 34 +- app/lib/auth/user.ts | 2 +- app/lib/kanban.ts | 16 +- app/lib/proxy/auth.ts | 12 +- app/lib/wakatime/repository.ts | 28 +- app/lib/wakatime/sync.ts | 50 +-- app/not-found.tsx | 6 +- app/page.tsx | 44 +-- app/sitemaps/leaderboards.ts | 2 +- app/utils/time.ts | 35 +- next-auth.d.ts | 8 +- .../20260728050901_kanban/migration.sql | 5 - .../migration.sql | 13 - .../migration.sql | 10 +- prisma/schema.prisma | 346 +++++++++--------- 99 files changed, 741 insertions(+), 889 deletions(-) delete mode 100644 prisma/migrations/20260728050901_kanban/migration.sql delete mode 100644 prisma/migrations/20260728120000_extend_kanban_projects/migration.sql rename prisma/migrations/{20260728043534_init => 20260822201501_init}/migration.sql (96%) diff --git a/app/(auth)/verify-wakatime/page.tsx b/app/(auth)/verify-wakatime/page.tsx index b47f453..af0ea06 100644 --- a/app/(auth)/verify-wakatime/page.tsx +++ b/app/(auth)/verify-wakatime/page.tsx @@ -15,7 +15,7 @@ export default async function VerifyWakatimePage() { return redirect("/login"); } - if (session.user.wakatimeApiKey) { + if (session.user.wakatime_api_key) { return redirect("/"); } diff --git a/app/(public)/(auth)/reset-password/page.tsx b/app/(public)/(auth)/reset-password/page.tsx index f65a5f4..fc4b7fb 100644 --- a/app/(public)/(auth)/reset-password/page.tsx +++ b/app/(public)/(auth)/reset-password/page.tsx @@ -88,7 +88,7 @@ export default async function ResetPassword() { const dev = - getAccount + getAccount ( this ); diff --git a/app/(public)/flex/page.tsx b/app/(public)/flex/page.tsx index c9390d8..5c3578d 100644 --- a/app/(public)/flex/page.tsx +++ b/app/(public)/flex/page.tsx @@ -58,8 +58,8 @@ export const metadata: Metadata = { export default async function Flexs() { const flexes = await prisma.userFlex.findMany({ - where: { expiresAt: { gt: new Date() } }, - orderBy: { createdAt: "desc" }, + where: { expires_at: { gt: new Date() } }, + orderBy: { created_at: "desc" }, }); return ( diff --git a/app/(public)/join/page.tsx b/app/(public)/join/page.tsx index 7a9846a..a71bcab 100644 --- a/app/(public)/join/page.tsx +++ b/app/(public)/join/page.tsx @@ -26,13 +26,15 @@ async function getLeaderboard(code: string) { description: true, slug: true, ownerId: true, - createdAt: true, + created_at: true, }, }); } async function getMemberCount(leaderboardId: string) { - return prisma.leaderboardMember.count({ where: { leaderboardId } }); + return prisma.leaderboardMember.count({ + where: { leaderboard_id: leaderboardId }, + }); } export async function generateMetadata({ @@ -83,10 +85,10 @@ export default async function JoinPage({ searchParams }: Props) { return (
-
+

@@ -139,9 +141,9 @@ export default async function JoinPage({ searchParams }: Props) { if (user) { const membership = await prisma.leaderboardMember.findUnique({ where: { - leaderboardId_userId: { - leaderboardId: leaderboard.id, - userId: user.id, + leaderboard_id_user_id: { + leaderboard_id: leaderboard.id, + user_id: user.id, }, }, select: { id: true }, @@ -151,18 +153,18 @@ export default async function JoinPage({ searchParams }: Props) { return (
-
+
-
+
Devpulse
-

+

{alreadyMember ? "You’re already a member of" : "You’ve been invited to"} @@ -182,7 +184,7 @@ export default async function JoinPage({ searchParams }: Props) {

{memberCount} {memberCount === 1 ? "member" : "members"} @@ -209,7 +211,7 @@ export default async function JoinPage({ searchParams }: Props) { Powered by{" "} Devpulse {" "} diff --git a/app/(public)/leaderboard/[slug]/page.tsx b/app/(public)/leaderboard/[slug]/page.tsx index c1992c5..0ee1e89 100644 --- a/app/(public)/leaderboard/[slug]/page.tsx +++ b/app/(public)/leaderboard/[slug]/page.tsx @@ -33,7 +33,7 @@ export default async function LeaderboardPage(props: { let members: NonNullableMember[] = []; try { const rows = await prisma.leaderboardMember.findMany({ - where: { leaderboardId: leaderboard.id }, + where: { leaderboard_id: leaderboard.id }, include: { user: { select: { diff --git a/app/(public)/leaderboard/page.tsx b/app/(public)/leaderboard/page.tsx index d79f2f6..db9fc6d 100644 --- a/app/(public)/leaderboard/page.tsx +++ b/app/(public)/leaderboard/page.tsx @@ -59,7 +59,7 @@ export const metadata: Metadata = { export default async function Leaderboards() { const leaderboards = await prisma.leaderboard.findMany({ select: { id: true, name: true, slug: true }, - orderBy: { createdAt: "desc" }, + orderBy: { created_at: "desc" }, }); return ( @@ -89,17 +89,17 @@ export default async function Leaderboards() {
-
+
{board.name}
- + View{" "} diff --git a/app/api/admin/stats/route.ts b/app/api/admin/stats/route.ts index ef4be2e..565d74a 100644 --- a/app/api/admin/stats/route.ts +++ b/app/api/admin/stats/route.ts @@ -21,16 +21,16 @@ export async function GET() { await Promise.all([ prisma.userStats.findMany({ select: { - userId: true, + user_id: true, totalSeconds: true, categories: true, user: { select: { email: true } }, }, }), prisma.conversation.count(), - prisma.message.count({ where: { expiresAt: { gt: new Date() } } }), + prisma.message.count({ where: { expires_at: { gt: new Date() } } }), prisma.leaderboard.count(), - prisma.userFlex.count({ where: { expiresAt: { gt: new Date() } } }), + prisma.userFlex.count({ where: { expires_at: { gt: new Date() } } }), ]); const users = topUserStats.map((row) => ({ diff --git a/app/api/auth/forgot-password/route.ts b/app/api/auth/forgot-password/route.ts index c5b967e..f8a5996 100644 --- a/app/api/auth/forgot-password/route.ts +++ b/app/api/auth/forgot-password/route.ts @@ -29,7 +29,7 @@ export async function POST(req: Request) { const expiresAt = new Date(Date.now() + 60 * 60 * 1000); const { token: resetToken } = await prisma.passwordResetToken.create({ - data: { userId: user.id, expiresAt }, + data: { user_id: user.id, expires_at: expiresAt }, select: { token: true }, }); diff --git a/app/api/auth/reset-password/route.ts b/app/api/auth/reset-password/route.ts index 4e07d17..bdd981f 100644 --- a/app/api/auth/reset-password/route.ts +++ b/app/api/auth/reset-password/route.ts @@ -35,7 +35,7 @@ export async function POST(req: Request) { where: { token: reset_token }, }); - if (!resetToken || resetToken.expiresAt < new Date()) { + if (!resetToken || resetToken.expires_at < new Date()) { return NextResponse.json( { error: "Invalid or expired reset link." }, { status: 400 }, @@ -46,7 +46,7 @@ export async function POST(req: Request) { await prisma.$transaction([ prisma.user.update({ - where: { id: resetToken.userId }, + where: { id: resetToken.user_id }, data: { password: hashed }, }), prisma.passwordResetToken.delete({ where: { token: reset_token } }), diff --git a/app/api/auth/verify-email/route.ts b/app/api/auth/verify-email/route.ts index f731f6f..1116d82 100644 --- a/app/api/auth/verify-email/route.ts +++ b/app/api/auth/verify-email/route.ts @@ -30,7 +30,7 @@ export async function POST(req: Request) { return NextResponse.json({ success: true }); } - if (user.emailVerified) { + if (user.email_verified) { return NextResponse.json({ success: true }); } @@ -93,7 +93,7 @@ export async function GET(req: Request) { await prisma.user.update({ where: { email: record.identifier }, - data: { emailVerified: new Date() }, + data: { email_verified: new Date() }, }); await prisma.verificationToken.delete({ where: { token } }); diff --git a/app/api/conversations/[id]/presence/route.ts b/app/api/conversations/[id]/presence/route.ts index f6cf3ea..8dc172b 100644 --- a/app/api/conversations/[id]/presence/route.ts +++ b/app/api/conversations/[id]/presence/route.ts @@ -25,7 +25,7 @@ export async function PATCH( } await prisma.conversationParticipant.updateMany({ - where: { conversationId, userId: session.user.id }, + where: { conversationId, user_id: session.user.id }, data, }); diff --git a/app/api/conversations/[id]/route.ts b/app/api/conversations/[id]/route.ts index 6be2a41..b6bdb33 100644 --- a/app/api/conversations/[id]/route.ts +++ b/app/api/conversations/[id]/route.ts @@ -15,7 +15,7 @@ export async function DELETE( const participant = await prisma.conversationParticipant.findUnique({ where: { - conversationId_userId: { conversationId: id, userId: session.user.id }, + conversationId_user_id: { conversationId: id, user_id: session.user.id }, }, }); diff --git a/app/api/conversations/[id]/unread/route.ts b/app/api/conversations/[id]/unread/route.ts index d83353f..84c8686 100644 --- a/app/api/conversations/[id]/unread/route.ts +++ b/app/api/conversations/[id]/unread/route.ts @@ -15,7 +15,7 @@ export async function GET( const participant = await prisma.conversationParticipant.findUnique({ where: { - conversationId_userId: { conversationId, userId: session.user.id }, + conversationId_user_id: { conversationId, user_id: session.user.id }, }, select: { lastReadAt: true }, }); @@ -28,8 +28,8 @@ export async function GET( where: { conversationId, senderId: { not: session.user.id }, - createdAt: { gt: participant.lastReadAt }, - expiresAt: { gt: new Date() }, + created_at: { gt: participant.lastReadAt }, + expires_at: { gt: new Date() }, }, }); diff --git a/app/api/conversations/route.ts b/app/api/conversations/route.ts index 88d929b..a1368aa 100644 --- a/app/api/conversations/route.ts +++ b/app/api/conversations/route.ts @@ -9,13 +9,13 @@ export async function GET() { } const participantRows = await prisma.conversationParticipant.findMany({ - where: { userId: session.user.id }, + where: { user_id: session.user.id }, include: { conversation: { include: { participants: { select: { - userId: true, + user_id: true, email: true, lastSeenAt: true, lastReadAt: true, @@ -65,13 +65,13 @@ export async function POST(req: Request) { participants: { create: [ { - userId: session.user.id, + user_id: session.user.id, email: session.user.email, lastSeenAt: timestamp, lastReadAt: timestamp, }, { - userId: otherUserId, + user_id: otherUserId, email: otherUserEmail ?? "", lastSeenAt: EPOCH, lastReadAt: EPOCH, @@ -81,7 +81,7 @@ export async function POST(req: Request) { }, include: { participants: { - select: { userId: true, email: true, lastSeenAt: true }, + select: { user_id: true, email: true, lastSeenAt: true }, }, }, }); diff --git a/app/api/cron/cleanup-messages/route.ts b/app/api/cron/cleanup-messages/route.ts index 0399665..8c62adc 100644 --- a/app/api/cron/cleanup-messages/route.ts +++ b/app/api/cron/cleanup-messages/route.ts @@ -8,7 +8,7 @@ export async function POST(req: Request) { } const { count } = await prisma.message.deleteMany({ - where: { expiresAt: { lt: new Date() } }, + where: { expires_at: { lt: new Date() } }, }); return NextResponse.json({ deleted: count }); diff --git a/app/api/flex/[id]/route.ts b/app/api/flex/[id]/route.ts index 591a808..2bff92c 100644 --- a/app/api/flex/[id]/route.ts +++ b/app/api/flex/[id]/route.ts @@ -23,14 +23,14 @@ export async function PUT( } = body; const flex = await prisma.userFlex.updateMany({ - where: { id, userId: session.user.id }, + where: { id, user_id: session.user.id }, data: { - projectName: project_name?.trim(), - projectDescription: project_description ?? "", - projectUrl: project_url ?? "", - projectTime: project_time ?? "", - isOpenSource: is_open_source ?? false, - openSourceUrl: is_open_source ? (open_source_url ?? "") : "", + project_name: project_name?.trim(), + project_description: project_description ?? "", + project_url: project_url ?? "", + project_time: project_time ?? "", + is_open_source: is_open_source ?? false, + open_source_url: is_open_source ? (open_source_url ?? "") : "", }, }); @@ -54,7 +54,7 @@ export async function DELETE( const { id } = await params; await prisma.userFlex.deleteMany({ - where: { id, userId: session.user.id }, + where: { id, user_id: session.user.id }, }); return NextResponse.json({ success: true }); diff --git a/app/api/flex/projects/route.ts b/app/api/flex/projects/route.ts index 92efc62..83af68e 100644 --- a/app/api/flex/projects/route.ts +++ b/app/api/flex/projects/route.ts @@ -9,7 +9,7 @@ export async function GET() { } const userProjects = await prisma.userProjects.findUnique({ - where: { userId: session.user.id }, + where: { user_id: session.user.id }, select: { projects: true }, }); diff --git a/app/api/flex/route.ts b/app/api/flex/route.ts index 2ad4644..609b87e 100644 --- a/app/api/flex/route.ts +++ b/app/api/flex/route.ts @@ -9,8 +9,8 @@ export async function GET() { } const flexes = await prisma.userFlex.findMany({ - where: { userId: session.user.id }, - orderBy: { createdAt: "desc" }, + where: { user_id: session.user.id }, + orderBy: { created_at: "desc" }, }); return NextResponse.json(flexes); @@ -43,14 +43,14 @@ export async function POST(req: Request) { const flex = await prisma.userFlex.create({ data: { - userId: session.user.id, + user_id: session.user.id, userEmail: session.user.email, - projectName: project_name.trim(), - projectDescription: project_description ?? "", - projectUrl: project_url ?? "", - projectTime: project_time ?? "", - isOpenSource: is_open_source ?? false, - openSourceUrl: is_open_source ? (open_source_url ?? "") : "", + project_name: project_name.trim(), + project_description: project_description ?? "", + project_url: project_url ?? "", + project_time: project_time ?? "", + is_open_source: is_open_source ?? false, + open_source_url: is_open_source ? (open_source_url ?? "") : "", expiresAt, }, }); diff --git a/app/api/kanban/projects/route.ts b/app/api/kanban/projects/route.ts index 58fc13a..52f36be 100644 --- a/app/api/kanban/projects/route.ts +++ b/app/api/kanban/projects/route.ts @@ -58,7 +58,7 @@ export async function POST(req: Request) { const boardId = crypto.randomUUID(); const safeDescription = body.description?.trim() || null; const safeWakaName = body.wakatimeProjectName?.trim() || null; - const safeColor = body.color?.trim() || "indigo"; + const safeColor = body.color?.trim() || "blue"; const now = new Date(); await prisma.$transaction(async (tx) => { diff --git a/app/api/leaderboards/[id]/leave/route.ts b/app/api/leaderboards/[id]/leave/route.ts index ec52d9b..d7b6161 100644 --- a/app/api/leaderboards/[id]/leave/route.ts +++ b/app/api/leaderboards/[id]/leave/route.ts @@ -14,7 +14,7 @@ export async function DELETE( const { id } = await params; await prisma.leaderboardMember.deleteMany({ - where: { leaderboardId: id, userId: session.user.id }, + where: { leaderboard_id: id, user_id: session.user.id }, }); return NextResponse.json({ success: true }); diff --git a/app/api/leaderboards/join/route.ts b/app/api/leaderboards/join/route.ts index 38d2656..a70df43 100644 --- a/app/api/leaderboards/join/route.ts +++ b/app/api/leaderboards/join/route.ts @@ -31,8 +31,8 @@ export async function POST(req: Request) { try { await prisma.leaderboardMember.create({ data: { - leaderboardId: leaderboard.id, - userId: session.user.id, + leaderboard_id: leaderboard.id, + user_id: session.user.id, role: "member", }, }); diff --git a/app/api/leaderboards/route.ts b/app/api/leaderboards/route.ts index 73b5999..d8ee22a 100644 --- a/app/api/leaderboards/route.ts +++ b/app/api/leaderboards/route.ts @@ -31,8 +31,8 @@ export async function POST(req: Request) { await prisma.leaderboardMember.create({ data: { - leaderboardId: leaderboard.id, - userId: session.user.id, + leaderboard_id: leaderboard.id, + user_id: session.user.id, role: "owner", }, }); diff --git a/app/api/messages/route.ts b/app/api/messages/route.ts index e60f3d6..8277c3f 100644 --- a/app/api/messages/route.ts +++ b/app/api/messages/route.ts @@ -21,7 +21,7 @@ export async function GET(req: Request) { const participant = await prisma.conversationParticipant.findUnique({ where: { - conversationId_userId: { conversationId, userId: session.user.id }, + conversationId_user_id: { conversationId, user_id: session.user.id }, }, }); @@ -32,9 +32,9 @@ export async function GET(req: Request) { const messages = await prisma.message.findMany({ where: { conversationId, - expiresAt: { gt: new Date() }, + expires_at: { gt: new Date() }, }, - orderBy: { createdAt: "asc" }, + orderBy: { created_at: "asc" }, }); return NextResponse.json( @@ -69,7 +69,7 @@ export async function POST(req: Request) { const participant = await prisma.conversationParticipant.findUnique({ where: { - conversationId_userId: { conversationId, userId: session.user.id }, + conversationId_user_id: { conversationId, user_id: session.user.id }, }, }); @@ -98,8 +98,8 @@ export async function POST(req: Request) { emitter.emit(`chat:${conversationId}`, { type: "message", data: payload }); const participants = await prisma.conversationParticipant.findMany({ - where: { conversationId, userId: { not: session.user.id } }, - select: { userId: true }, + where: { conversationId, user_id: { not: session.user.id } }, + select: { user_id: true }, }); for (const p of participants) { diff --git a/app/api/presence/route.ts b/app/api/presence/route.ts index c64c7a2..bbc9df7 100644 --- a/app/api/presence/route.ts +++ b/app/api/presence/route.ts @@ -11,7 +11,7 @@ export async function PATCH() { const timestamp = new Date(); await prisma.conversationParticipant.updateMany({ - where: { userId: session.user.id }, + where: { user_id: session.user.id }, data: { lastSeenAt: timestamp }, }); diff --git a/app/api/sse/chat/[conversationId]/route.ts b/app/api/sse/chat/[conversationId]/route.ts index 6325b20..4919f3c 100644 --- a/app/api/sse/chat/[conversationId]/route.ts +++ b/app/api/sse/chat/[conversationId]/route.ts @@ -18,7 +18,7 @@ export async function GET( const participant = await prisma.conversationParticipant.findUnique({ where: { - conversationId_userId: { conversationId, userId: session.user.id }, + conversationId_user_id: { conversationId, user_id: session.user.id }, }, }); diff --git a/app/api/sse/chat/[conversationId]/typing/route.ts b/app/api/sse/chat/[conversationId]/typing/route.ts index 51b3de6..5d0e2f1 100644 --- a/app/api/sse/chat/[conversationId]/typing/route.ts +++ b/app/api/sse/chat/[conversationId]/typing/route.ts @@ -16,7 +16,7 @@ export async function POST( const participant = await prisma.conversationParticipant.findUnique({ where: { - conversationId_userId: { conversationId, userId: session.user.id }, + conversationId_user_id: { conversationId, user_id: session.user.id }, }, }); diff --git a/app/api/users/badges/route.ts b/app/api/users/badges/route.ts index fbe884e..98893b5 100644 --- a/app/api/users/badges/route.ts +++ b/app/api/users/badges/route.ts @@ -16,8 +16,8 @@ export async function GET(req: Request) { } const stats = await prisma.userStats.findMany({ - where: { userId: { in: ids } }, - select: { userId: true, totalSeconds: true }, + where: { user_id: { in: ids } }, + select: { user_id: true, totalSeconds: true }, }); return NextResponse.json( diff --git a/app/api/users/route.ts b/app/api/users/route.ts index 4b6c976..58762ed 100644 --- a/app/api/users/route.ts +++ b/app/api/users/route.ts @@ -21,9 +21,9 @@ export async function GET(req: Request) { const participants = await prisma.conversationParticipant.findMany({ where: { conversationId, - userId: { not: session.user.id }, + user_id: { not: session.user.id }, }, - select: { userId: true, email: true }, + select: { user_id: true, email: true }, }); const users = participants diff --git a/app/api/wakatime/sync/route.ts b/app/api/wakatime/sync/route.ts index 6700258..b284c44 100644 --- a/app/api/wakatime/sync/route.ts +++ b/app/api/wakatime/sync/route.ts @@ -39,7 +39,7 @@ export async function GET(request: Request) { const result = await syncWakatimeData({ userId: user.id, incomingApiKey: apiKey, - storedApiKey: user.wakatimeApiKey, + storedApiKey: user.wakatime_api_key, }); if (!result.success && result.status !== 200) { diff --git a/app/components/BoardList.tsx b/app/components/BoardList.tsx index 0acfe1b..25aac6d 100644 --- a/app/components/BoardList.tsx +++ b/app/components/BoardList.tsx @@ -129,10 +129,10 @@ export default function BoardList({ href={`/leaderboard/${board.slug}`} className="flex-1 flex items-center min-w-0 pr-4" > -
+
@@ -141,7 +141,7 @@ export default function BoardList({ {board.name}

-

+

/{board.slug}

@@ -149,7 +149,7 @@ export default function BoardList({
@@ -158,7 +158,7 @@ export default function BoardList({

@@ -387,7 +387,7 @@ export default function Chat({ user }: { user: ChatUserShape }) { value={messageSearch} onChange={(e) => setMessageSearch(e.target.value)} placeholder="Search Message..." - className="w-full bg-gray-50 border border-transparent rounded-xl py-2 pl-9 pr-4 text-sm text-gray-700 placeholder:text-gray-500 outline-none focus:border-indigo-500/50 transition-colors shadow-inner" + className="w-full bg-gray-50 border border-transparent rounded-xl py-2 pl-9 pr-4 text-sm text-gray-700 placeholder:text-gray-500 outline-none focus:border-blue-500/50 transition-colors shadow-inner" />
@@ -436,7 +436,7 @@ export default function Chat({ user }: { user: ChatUserShape }) { setDmSortOrder("newest"); setIsDmSortOpen(false); }} - className={`w-full text-left px-3 py-1.5 hover:bg-gray-100 transition-colors ${dmSortOrder === "newest" ? "text-indigo-600 bg-gray-50" : "text-gray-600"}`} + className={`w-full text-left px-3 py-1.5 hover:bg-gray-100 transition-colors ${dmSortOrder === "newest" ? "text-blue-600 bg-gray-50" : "text-gray-600"}`} > Newest @@ -445,7 +445,7 @@ export default function Chat({ user }: { user: ChatUserShape }) { setDmSortOrder("oldest"); setIsDmSortOpen(false); }} - className={`w-full text-left px-3 py-1.5 hover:bg-gray-100 transition-colors ${dmSortOrder === "oldest" ? "text-indigo-600 bg-gray-50" : "text-gray-600"}`} + className={`w-full text-left px-3 py-1.5 hover:bg-gray-100 transition-colors ${dmSortOrder === "oldest" ? "text-blue-600 bg-gray-50" : "text-gray-600"}`} > Oldest @@ -454,7 +454,7 @@ export default function Chat({ user }: { user: ChatUserShape }) { setDmSortOrder("az"); setIsDmSortOpen(false); }} - className={`w-full text-left px-3 py-1.5 hover:bg-gray-100 transition-colors ${dmSortOrder === "az" ? "text-indigo-600 bg-gray-50" : "text-gray-600"}`} + className={`w-full text-left px-3 py-1.5 hover:bg-gray-100 transition-colors ${dmSortOrder === "az" ? "text-blue-600 bg-gray-50" : "text-gray-600"}`} > A-Z @@ -463,7 +463,7 @@ export default function Chat({ user }: { user: ChatUserShape }) { setDmSortOrder("za"); setIsDmSortOpen(false); }} - className={`w-full text-left px-3 py-1.5 hover:bg-gray-100 transition-colors ${dmSortOrder === "za" ? "text-indigo-600 bg-gray-50" : "text-gray-600"}`} + className={`w-full text-left px-3 py-1.5 hover:bg-gray-100 transition-colors ${dmSortOrder === "za" ? "text-blue-600 bg-gray-50" : "text-gray-600"}`} > Z-A @@ -505,7 +505,7 @@ export default function Chat({ user }: { user: ChatUserShape }) {
{activeInitials}
@@ -525,7 +525,7 @@ export default function Chat({ user }: { user: ChatUserShape }) {
@@ -646,7 +646,7 @@ export default function Chat({ user }: { user: ChatUserShape }) { className={`h-10 px-5 rounded-[20px] font-semibold text-[14px] flex items-center gap-2.5 transition-all duration-300 flex-shrink-0 ${ canSendMessage - ? "bg-gradient-to-r from-indigo-500 to-violet-500 hover:from-indigo-400 hover:to-violet-400 text-gray-900 shadow-md shadow-indigo-500/25 active:scale-95" + ? "bg-gradient-to-r from-blue-500 to-violet-500 hover:from-blue-400 hover:to-violet-400 text-gray-900 shadow-md shadow-blue-500/25 active:scale-95" : "bg-gray-50 text-gray-500 cursor-not-allowed" } `} @@ -665,7 +665,7 @@ export default function Chat({ user }: { user: ChatUserShape }) { ) : (
-
+

@@ -694,7 +694,7 @@ export default function Chat({ user }: { user: ChatUserShape }) {

{activeInitials}
@@ -713,7 +713,7 @@ export default function Chat({ user }: { user: ChatUserShape }) { input.focus(); } }} - className="w-11 h-11 rounded-2xl bg-gray-50 flex items-center justify-center text-gray-500 hover:text-indigo-600 hover:bg-gray-100 transition shadow-sm" + className="w-11 h-11 rounded-2xl bg-gray-50 flex items-center justify-center text-gray-500 hover:text-blue-600 hover:bg-gray-100 transition shadow-sm" title="Search Message" > @@ -806,7 +806,7 @@ export default function Chat({ user }: { user: ChatUserShape }) { {allMediaAttachments.length > 9 && !showAllMedia && ( @@ -821,7 +821,7 @@ export default function Chat({ user }: { user: ChatUserShape }) { )} ) : ( -
+
createConversation(u)} className="flex items-center gap-3 p-2.5 rounded-xl hover:bg-gray-100 cursor-pointer transition" > -
+
{u.email[0].toUpperCase()}
diff --git a/app/components/Flex.tsx b/app/components/Flex.tsx index 32b626c..e7acca1 100644 --- a/app/components/Flex.tsx +++ b/app/components/Flex.tsx @@ -27,14 +27,14 @@ export interface Projects { interface FlexRow { id: string; - projectName: string; - projectDescription: string; - projectUrl: string; - projectTime: string; - isOpenSource: boolean; - openSourceUrl: string; - expiresAt: string; - createdAt: string; + project_name: string; + project_description: string; + project_url: string; + project_time: string; + is_open_source: boolean; + open_source_url: string; + expires_at: string; + created_at: string; } function toEditableFlex(row: FlexRow): Projects { @@ -199,10 +199,10 @@ export default function Flex() {
-

+

Flex

diff --git a/app/components/JoinButton.tsx b/app/components/JoinButton.tsx index 73c7ec0..8b0b801 100644 --- a/app/components/JoinButton.tsx +++ b/app/components/JoinButton.tsx @@ -30,7 +30,7 @@ export default function JoinButton({ return ( View @@ -44,7 +44,7 @@ export default function JoinButton({

Log In to Join @@ -53,7 +53,7 @@ export default function JoinButton({ Don't have an account?{" "} Sign up free @@ -97,7 +97,7 @@ export default function JoinButton({
- import + import {"{ Metrics }"} - from + from '@devpulse/core' @@ -107,14 +107,14 @@ export default function Login() { -
+
Forgot your password? diff --git a/app/components/auth/Logout.tsx b/app/components/auth/Logout.tsx index 95ae438..6c38aa3 100644 --- a/app/components/auth/Logout.tsx +++ b/app/components/auth/Logout.tsx @@ -23,7 +23,7 @@ export default function Logout() { return (
-
+
); } diff --git a/app/components/auth/Oauth2.tsx b/app/components/auth/Oauth2.tsx index 685b8b9..37fcea6 100644 --- a/app/components/auth/Oauth2.tsx +++ b/app/components/auth/Oauth2.tsx @@ -19,7 +19,7 @@ export default function Oauth2({ redirectTo }: { redirectTo: string }) { type="button" onClick={() => handleOAuth("google")} disabled - className="disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 w-full py-3 rounded-lg border border-gray-200 bg-gray-50 text-gray-700 hover:bg-gray-100 hover:border-gray-300 transition-colors shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/60" + className="disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 w-full py-3 rounded-lg border border-gray-200 bg-gray-50 text-gray-700 hover:bg-gray-100 hover:border-gray-300 transition-colors shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/60" > Google @@ -29,7 +29,7 @@ export default function Oauth2({ redirectTo }: { redirectTo: string }) { type="button" onClick={() => handleOAuth("microsoft-entra-id")} disabled - className="disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 w-full py-3 rounded-lg border border-gray-200 bg-gray-50 text-gray-700 hover:bg-gray-100 hover:border-gray-300 transition-colors shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/60" + className="disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 w-full py-3 rounded-lg border border-gray-200 bg-gray-50 text-gray-700 hover:bg-gray-100 hover:border-gray-300 transition-colors shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/60" > Microsoft @@ -39,7 +39,7 @@ export default function Oauth2({ redirectTo }: { redirectTo: string }) { type="button" onClick={() => handleOAuth("github")} disabled - className="disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 w-full py-3 rounded-lg border border-gray-200 bg-gray-50 text-gray-700 hover:bg-gray-100 hover:border-gray-300 transition-colors shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/60" + className="disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 w-full py-3 rounded-lg border border-gray-200 bg-gray-50 text-gray-700 hover:bg-gray-100 hover:border-gray-300 transition-colors shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/60" > GitHub diff --git a/app/components/auth/Signup.tsx b/app/components/auth/Signup.tsx index 5256ec8..6d003ab 100644 --- a/app/components/auth/Signup.tsx +++ b/app/components/auth/Signup.tsx @@ -59,7 +59,7 @@ export default function Signup() { const dev = - new + new Developer ();
@@ -117,7 +117,7 @@ export default function Signup() { ? `/login?redirect=${encodeURIComponent(redirectTo)}` : "/login" } - className="text-indigo-600 hover:text-indigo-600 font-semibold transition-colors underline-offset-4 hover:underline" + className="text-blue-600 hover:text-blue-600 font-semibold transition-colors underline-offset-4 hover:underline" > Log in diff --git a/app/components/auth/VerifyEmail.tsx b/app/components/auth/VerifyEmail.tsx index 07d5ec8..f5628d5 100644 --- a/app/components/auth/VerifyEmail.tsx +++ b/app/components/auth/VerifyEmail.tsx @@ -199,7 +199,7 @@ export default function VerifyEmail({ )} Changed your mind? Log in again. @@ -207,9 +207,9 @@ export default function VerifyEmail({ ) : ( <>
-
+
-
+
@@ -79,7 +79,7 @@ export default function Conversations({
{isGlobal && ( - + All )} @@ -92,7 +92,7 @@ export default function Conversations({
diff --git a/app/components/chat/Messages.tsx b/app/components/chat/Messages.tsx index ba9ea9a..b95a8fe 100644 --- a/app/components/chat/Messages.tsx +++ b/app/components/chat/Messages.tsx @@ -25,7 +25,7 @@ export default function Messages({ conversations: Conversation[]; bottomRef: React.RefObject; badgesByUserId?: Record; - onUserProfileClick?: (targetUserId: string, targetEmail: string) => void; + onUserProfileClick?: (targetuser_id: string, targetEmail: string) => void; }) { const [showScrollBtn, setShowScrollBtn] = useState(false); const [mediaViewer, setMediaViewer] = useState( @@ -157,7 +157,7 @@ export default function Messages({ title={canOpenPrivateChat ? "Start private chat" : undefined} className={`flex-shrink-0 ${avatarTranslateClass} w-8 h-8 rounded-full bg-neutral-700 border border-gray-200 flex items-center justify-center aspect-square overflow-hidden ${ canOpenPrivateChat - ? "cursor-pointer hover:border-indigo-400/60 hover:bg-neutral-700/80" + ? "cursor-pointer hover:border-blue-400/60 hover:bg-neutral-700/80" : "" }`} > @@ -189,7 +189,7 @@ export default function Messages({ senderRow.email as string, ) } - className="text-[12px] font-semibold leading-none text-gray-700 hover:text-indigo-600 transition" + className="text-[12px] font-semibold leading-none text-gray-700 hover:text-blue-600 transition" title="Start private chat" > {senderName} @@ -197,7 +197,7 @@ export default function Messages({ ) : ( {senderName} @@ -225,8 +225,8 @@ export default function Messages({
@@ -430,7 +430,7 @@ function getAttachments( href={attachment.public_url} target="_blank" rel="noopener noreferrer" - className="inline-flex items-center gap-1.5 text-indigo-600 hover:text-indigo-200 hover:underline text-sm" + className="inline-flex items-center gap-1.5 text-blue-600 hover:text-blue-200 hover:underline text-sm" > {attachment.filename || "Open attachment"} diff --git a/app/components/chat/Player.tsx b/app/components/chat/Player.tsx index cc53426..ca8f2e8 100644 --- a/app/components/chat/Player.tsx +++ b/app/components/chat/Player.tsx @@ -601,7 +601,7 @@ export default function Player({ onMouseUp={() => setIsSeeking(false)} onTouchStart={() => setIsSeeking(true)} onTouchEnd={() => setIsSeeking(false)} - className="flex-1 accent-indigo-400 cursor-pointer h-1" + className="flex-1 accent-blue-400 cursor-pointer h-1" aria-label="Seek video" style={{ background: `linear-gradient(90deg, rgba(129,140,248,.95) ${progress}%, rgba(255,255,255,.22) ${progress}%)`, @@ -667,7 +667,7 @@ export default function Player({ step={0.01} value={volume} onChange={(e) => setVolume(Number(e.target.value))} - className="h-24 w-1.5 accent-indigo-400 cursor-pointer" + className="h-24 w-1.5 accent-blue-400 cursor-pointer" aria-label="Mobile volume" style={{ writingMode: "vertical-lr", direction: "rtl" }} /> @@ -683,7 +683,7 @@ export default function Player({ step={0.01} value={volume} onChange={(e) => setVolume(Number(e.target.value))} - className="hidden md:block w-20 accent-indigo-400 h-1" + className="hidden md:block w-20 accent-blue-400 h-1" aria-label="Volume" /> @@ -727,7 +727,7 @@ export default function Player({ > diff --git a/app/components/dashboard/LeaderbordList.tsx b/app/components/dashboard/LeaderbordList.tsx index 8aeceae..89b7407 100644 --- a/app/components/dashboard/LeaderbordList.tsx +++ b/app/components/dashboard/LeaderbordList.tsx @@ -21,7 +21,7 @@ export default async function LeaderboardsList() { select: { id: true, name: true, slug: true, ownerId: true }, }), prisma.leaderboardMember.findMany({ - where: { userId: user.id, role: "member" }, + where: { user_id: user.id, role: "member" }, include: { leaderboard: { select: { id: true, name: true, slug: true, ownerId: true }, @@ -39,17 +39,17 @@ export default async function LeaderboardsList() { return (
-
+

Your Networks

diff --git a/app/components/dashboard/Navbar.tsx b/app/components/dashboard/Navbar.tsx index 1eb75e8..5c1a62a 100644 --- a/app/components/dashboard/Navbar.tsx +++ b/app/components/dashboard/Navbar.tsx @@ -205,7 +205,7 @@ export default function DashboardLayout({ text-sm font-medium transition ${ pathname === item.href - ? "bg-indigo-50 text-indigo-600 border border-indigo-200" + ? "bg-blue-50 text-blue-600 border border-blue-200" : "text-gray-500 hover:text-gray-600 hover:bg-gray-100" }`} target={item.href.startsWith("http") ? "_blank" : undefined} @@ -214,7 +214,7 @@ export default function DashboardLayout({ {item.label} diff --git a/app/components/dashboard/Settings/Profile.tsx b/app/components/dashboard/Settings/Profile.tsx index 85fdae3..e59cc2d 100644 --- a/app/components/dashboard/Settings/Profile.tsx +++ b/app/components/dashboard/Settings/Profile.tsx @@ -79,10 +79,10 @@ export default function UserProfile({ user }: { user: UserShape }) { } return ( -
+
-

+

Account Profile

diff --git a/app/components/dashboard/Settings/WakaTimeKey.tsx b/app/components/dashboard/Settings/WakaTimeKey.tsx index 442d415..ff0e6ef 100644 --- a/app/components/dashboard/Settings/WakaTimeKey.tsx +++ b/app/components/dashboard/Settings/WakaTimeKey.tsx @@ -154,7 +154,7 @@ export default function WakaTimeKey({ WakaTime account settings diff --git a/app/components/dashboard/Stats.tsx b/app/components/dashboard/Stats.tsx index e07528e..39dc733 100644 --- a/app/components/dashboard/Stats.tsx +++ b/app/components/dashboard/Stats.tsx @@ -29,7 +29,7 @@ export interface StatsData { projects?: { name: string; total_seconds: number }[]; daily_stats?: { date: string; total_seconds: number }[]; best_day?: { date: string; total_seconds: number }; - last_fetched_at?: string; + last_fetched_at: string; } export default function Stats() { @@ -56,6 +56,7 @@ export default function Stats() { projects: [], daily_stats: [], best_day: { date: "", total_seconds: 0 }, + last_fetched_at: "", }); const fetchStats = useCallback( @@ -119,17 +120,12 @@ export default function Stats() { useEffect(() => { if (!syncing && hasLoadedData) { - const aosTimer = setTimeout(() => { - AOS.refresh(); - }, 200); - // Even progress bars need a tiny warm-up lap. const timer = setTimeout(() => { setAnimated(true); }, 120); return () => { - clearTimeout(aosTimer); clearTimeout(timer); }; } @@ -201,41 +197,7 @@ export default function Stats() { ); const topLangProgress = stats.languages[0]?.percent || 0; const topEditorProgress = stats.editors[0]?.percent || 0; - const WEEKLY_GOAL_HOURS = 20; - const weeklyGoalSeconds = WEEKLY_GOAL_HOURS * 3600; - const last7Seconds = lastSevenDailyStats.reduce( - (sum, day) => sum + day.total_seconds, - 0, - ); - const prevSevenDailyStats = - sortedDailyStats.length > 7 ? sortedDailyStats.slice(-14, -7) : []; - const prev7Seconds = prevSevenDailyStats.reduce( - (sum, day) => sum + day.total_seconds, - 0, - ); - const weeklyGoalPercent = - weeklyGoalSeconds > 0 ? (last7Seconds / weeklyGoalSeconds) * 100 : 0; - const activeDaysThisWeek = lastSevenDailyStats.filter( - (d) => d.total_seconds > 0, - ).length; - const avgActiveDaySeconds = - activeDaysThisWeek > 0 ? last7Seconds / activeDaysThisWeek : 0; - - const peakDayThisWeek = dailyData.reduce( - (max, day) => (day.hours > max.hours ? day : max), - dailyData[0] || { day: "N/A", hours: 0 }, - ); - - let momentumPercent = 0; - if (prev7Seconds > 0) { - momentumPercent = ((last7Seconds - prev7Seconds) / prev7Seconds) * 100; - } else if (last7Seconds > 0) { - momentumPercent = 100; - } - const momentumLabel = `${momentumPercent >= 0 ? "+" : ""}${momentumPercent.toFixed(0)}%`; - const momentumClass = - momentumPercent >= 0 ? "text-emerald-300" : "text-rose-300"; const bestDayDate = stats.best_day?.date || ""; const bestDaySeconds = stats.best_day?.total_seconds || 0; const hasBestDayData = !!bestDayDate && bestDaySeconds > 0; @@ -249,45 +211,35 @@ export default function Stats() { label: "Total Coding", value: totalHoursFormatted, sub: "Last 7 days", - color: "#6366f1", trend: `${totalCodingProgress.toFixed(0)}%`, - trendUp: true, progress: totalCodingProgress, }, { label: "Daily Average", value: avgDailyFormatted, sub: "Per day", - color: "#8b5cf6", trend: `${dailyAverageProgress.toFixed(0)}%`, - trendUp: true, progress: dailyAverageProgress, }, { label: "Top Language", value: topLang, sub: formatHours(stats.languages[0]?.total_seconds || 0), - color: "#22d3ee", trend: `${topLangProgress.toFixed(0)}%`, - trendUp: true, progress: topLangProgress, }, { label: "Editor", value: topEditor, sub: formatHours(stats.editors[0]?.total_seconds || 0), - color: "#34d399", trend: `${topEditorProgress.toFixed(0)}%`, - trendUp: true, progress: topEditorProgress, }, { label: "Best Day", value: bestDayValue, sub: bestDaySub, - color: "#f59e0b", trend: "Top", - trendUp: true, progress: hasBestDayData ? 100 : 0, }, ]; @@ -295,162 +247,73 @@ export default function Stats() { /** * i think ive seen this code before... where was it... hmmm... oh yeah, i wrote it like 5 minutes ago in the StatsCard component. maybe i should just move this logic there? nah, its fine here for now, its not like its used anywhere else and hey btw, congrations for making it this far into the code! you must be really interested in how this dashboard works. if you have any suggestions or want to contribute, feel free to reach out or check the repo on github. happy coding! Ohhh your still reading this comment? well i guess i can share a little secret with you... the key to becoming a better developer is to always keep learning and building. don't be afraid to experiment, break things, and learn from your mistakes. also, remember to take breaks and have fun with coding! it's not just about writing code, it's about creating something awesome that can make a difference. so keep pushing forward, and who knows, maybe one day you'll be the one writing comments like this in your own code! hahaha - the DevPulse Team */ - return (

- {/* Header */} -
-
-

- Devpulse -

-

- - Your coding activity overview -

-
+
+ {/* Main Left Content */} +
+ {/* Top KPI Cards Row */} + + + {/* Primary Metrics (Charts) - 2 Columns */} +
+ +
+ +
+
-
- {/* Sync Button as an icon button */} - -
-
+ - {syncing ? ( -
-
-
-

- Synchronizing data... -

+ {/* Core Codebase Breakdown */} +
+ +
- ) : ( -
- {/* Main Left Content */} -
- {/* Top KPI Cards Row */} - - - {/* Primary Metrics (Charts) - 2 Columns */} -
- -
- -
-
- + {/* Right Sidebar: Environment & Tools */} +
+
+
+
+

+ {stats.last_fetched_at + ? new Date(stats.last_fetched_at).toLocaleString() + : "—"} +

+ +
- {/* Core Codebase Breakdown */} -
- - -
-
+ - {/* Right Sidebar: Environment & Tools */} -
-
-
-
- -
-
- -
-
- -
-
- -
-
-
+ -
-

- Performance Signals -

-
-
- Weekly Goal - - {weeklyGoalPercent.toFixed(0)}% - -
-
- Goal Progress - - {formatHours(last7Seconds)} / {WEEKLY_GOAL_HOURS}h - -
-
- Momentum vs Prev 7d - - {momentumLabel} - -
-
- Active Days (7d) - - {activeDaysThisWeek} / 7 - -
-
- Avg Active Day - - {formatHours(avgActiveDaySeconds)} - -
-
- Peak Day This Week - - {peakDayThisWeek.day}{" "} - {peakDayThisWeek.hours > 0 - ? `(${formatHours(peakDayThisWeek.hours * 3600)})` - : ""} - -
-
+ -
-
-
+
- )} +
); } diff --git a/app/components/dashboard/widgets/Categories.tsx b/app/components/dashboard/widgets/Categories.tsx index db01e4b..2c464ee 100644 --- a/app/components/dashboard/widgets/Categories.tsx +++ b/app/components/dashboard/widgets/Categories.tsx @@ -17,7 +17,7 @@ export default function Categories({ return ( <> -
+

Categories

{categoriesList.slice(0, 4).map((category, idx) => { @@ -31,7 +31,7 @@ export default function Categories({ {category.name} {idx === 0 && ( - + MAIN )} @@ -41,15 +41,15 @@ export default function Categories({ {formatHours(category.total_seconds)} - + {formatPercent(percent)}
-
+
diff --git a/app/components/dashboard/widgets/CodingActivity.tsx b/app/components/dashboard/widgets/CodingActivity.tsx index 8a3a5e2..551e15e 100644 --- a/app/components/dashboard/widgets/CodingActivity.tsx +++ b/app/components/dashboard/widgets/CodingActivity.tsx @@ -18,11 +18,13 @@ export default function CodingActivity({ <>
-

Coding Activity

+

+ Coding Activity +

Last 7 days
@@ -33,8 +35,8 @@ export default function CodingActivity({ > - - + + [ formatHours((value as number) * 3600), "Time", @@ -71,7 +72,7 @@ export default function CodingActivity({ diff --git a/app/components/dashboard/widgets/CodingConsistencyHeatmap.tsx b/app/components/dashboard/widgets/CodingConsistencyHeatmap.tsx index bbdb0c5..bd29ded 100644 --- a/app/components/dashboard/widgets/CodingConsistencyHeatmap.tsx +++ b/app/components/dashboard/widgets/CodingConsistencyHeatmap.tsx @@ -44,10 +44,10 @@ function getCellTone(seconds: number) { const hours = seconds / 3600; if (hours <= 0) return "bg-gray-100 border border-gray-200"; - if (hours < 0.5) return "bg-indigo-100 border border-indigo-200"; - if (hours < 1.5) return "bg-indigo-200 border border-indigo-300"; - if (hours < 3) return "bg-indigo-400 border border-indigo-500"; - return "bg-indigo-600 border border-indigo-700"; + if (hours < 0.5) return "bg-blue-100 border border-blue-200"; + if (hours < 1.5) return "bg-blue-200 border border-blue-300"; + if (hours < 3) return "bg-blue-400 border border-blue-500"; + return "bg-blue-600 border border-blue-700"; } export default function CodingConsistencyHeatmap({ @@ -147,7 +147,7 @@ export default function CodingConsistencyHeatmap({ return (
@@ -210,7 +210,7 @@ export default function CodingConsistencyHeatmap({ return (
Less - - - - + + + + More
@@ -267,19 +267,19 @@ export default function CodingConsistencyHeatmap({

Consistency:{" "} - + {consistencyScore}%

Current streak:{" "} - + {currentStreak}

Best streak:{" "} - {bestStreak} + {bestStreak}

diff --git a/app/components/dashboard/widgets/Dependencies.tsx b/app/components/dashboard/widgets/Dependencies.tsx index af0134d..2fed9e2 100644 --- a/app/components/dashboard/widgets/Dependencies.tsx +++ b/app/components/dashboard/widgets/Dependencies.tsx @@ -14,7 +14,7 @@ export default function Dependencies({ return ( <> -
+

Dependencies

@@ -30,18 +30,18 @@ export default function Dependencies({ {dep.name}
-
+
{formatHours(dep.total_seconds)} - + {dep.percent.toFixed(0)}%
-
+
diff --git a/app/components/dashboard/widgets/Editors.tsx b/app/components/dashboard/widgets/Editors.tsx index 4916476..6c7a3a5 100644 --- a/app/components/dashboard/widgets/Editors.tsx +++ b/app/components/dashboard/widgets/Editors.tsx @@ -30,7 +30,7 @@ export default function Editors({ return ( <> -
+

Editors

{editorsList.slice(0, 4).map((editor, idx) => { @@ -43,7 +43,7 @@ export default function Editors({ @@ -51,7 +51,7 @@ export default function Editors({ {editor.name} {idx === 0 && ( - + PRIMARY )} @@ -61,16 +61,15 @@ export default function Editors({ {formatHours(editor.total_seconds)} - + {formatPercent(percent)}
- {/* The bars only run after warm-up stretches. */} -
+
diff --git a/app/components/dashboard/widgets/LanguageDestribution.tsx b/app/components/dashboard/widgets/LanguageDestribution.tsx index 1c1978f..57cf2ac 100644 --- a/app/components/dashboard/widgets/LanguageDestribution.tsx +++ b/app/components/dashboard/widgets/LanguageDestribution.tsx @@ -48,8 +48,8 @@ export default function LanguageDestribution({

{point.subject}

-

{point.percent}% share

-

{formatHours(point.seconds)}

+

{point.percent}% share

+

{formatHours(point.seconds)}

); }; @@ -58,7 +58,7 @@ export default function LanguageDestribution({ return (

@@ -73,7 +73,7 @@ export default function LanguageDestribution({ <>

@@ -105,14 +105,14 @@ export default function LanguageDestribution({ /> -
+

Machines

{machinesList.slice(0, 4).map((machine, idx) => (
- + @@ -37,14 +37,14 @@ export default function Machines({ {formatHours(machine.total_seconds)} - + {formatPercent(machine.percent)}
-
+
diff --git a/app/components/dashboard/widgets/OperatingSystem.tsx b/app/components/dashboard/widgets/OperatingSystem.tsx index 76a68c5..a1c3f64 100644 --- a/app/components/dashboard/widgets/OperatingSystem.tsx +++ b/app/components/dashboard/widgets/OperatingSystem.tsx @@ -39,7 +39,7 @@ export default function OperatingSystem({ return ( <> -
+

Operating Systems

@@ -54,7 +54,7 @@ export default function OperatingSystem({ @@ -62,7 +62,7 @@ export default function OperatingSystem({ {os.name} {idx === 0 && ( - + MAIN )} @@ -72,15 +72,15 @@ export default function OperatingSystem({ {formatHours(os.total_seconds)} - + {formatPercent(percent)}
-
+
diff --git a/app/components/dashboard/widgets/Projects.tsx b/app/components/dashboard/widgets/Projects.tsx index 951a040..3e0c486 100644 --- a/app/components/dashboard/widgets/Projects.tsx +++ b/app/components/dashboard/widgets/Projects.tsx @@ -8,68 +8,58 @@ export default function Projects({ stats: StatsData; animated: boolean; }) { - const totalProjectSeconds = (stats.projects || []).reduce( + const projectsList = stats.projects || []; + const totalProjectSeconds = projectsList.reduce( (acc, curr) => acc + curr.total_seconds, 0, ); return ( <> - {stats.projects && stats.projects.length > 0 ? ( - <> -
-

- Top Projects -

-
- {stats.projects.slice(0, 6).map((project, idx) => { - const percent = - totalProjectSeconds > 0 - ? (project.total_seconds / totalProjectSeconds) * 100 - : 0; - return ( -
-
- - {project.name} - -
- - {formatHours(project.total_seconds)} - - - {percent.toFixed(0)}% - -
-
-
-
-
-
- ); - })} -
-
- - ) : ( -
+
+

Top Projects

-
-

No project data available.

+ +
+ {projectsList.slice(0, 6).map((project, idx) => { + const percent = + totalProjectSeconds > 0 + ? (project.total_seconds / totalProjectSeconds) * 100 + : 0; + return ( +
+
+ + {project.name} + +
+ + {formatHours(project.total_seconds)} + + + {percent.toFixed(0)}% + +
+
+
+
+
+
+ ); + })} + {projectsList.length === 0 && ( +

+ No project data. +

+ )}
- )} +
); } diff --git a/app/components/dashboard/widgets/StatsCard.tsx b/app/components/dashboard/widgets/StatsCard.tsx index e86a2cd..7314a5a 100644 --- a/app/components/dashboard/widgets/StatsCard.tsx +++ b/app/components/dashboard/widgets/StatsCard.tsx @@ -1,11 +1,10 @@ "use client"; + export interface StatCard { label: string; value: string; sub: string; trend: string; - trendUp: boolean; - color: string; progress: number; // 0 to 100 } @@ -19,44 +18,28 @@ export default function StatsCard({ setAnimated: (val: boolean) => void; }) { return ( -
+
{statCards.map((card, idx) => (
0 - ? "xl:border-l xl:pl-8 xl:pt-0 xl:border-t-0 border-gray-800/50 " - : "" - }${ - idx % 2 !== 0 - ? "sm:border-l sm:pl-8 border-gray-800/50 " - : "sm:pr-8 xl:pr-0 " - }${idx >= 2 ? "sm:border-t sm:pt-8 border-gray-800/50 " : ""}${ - idx === 4 - ? "sm:col-span-2 xl:col-span-1 sm:border-l-0 sm:pl-0 sm:pr-0 xl:border-l xl:pl-8" - : "" - }`} + className="group flex flex-col pr-8 last:pr-0 border-gray-800/50" >

{card.label}

- + {card.trend}
-

{card.value}

+

+ {card.value} +

{card.sub}

{/* Mini bar */}
{ setAnimated(false); @@ -64,10 +47,9 @@ export default function StatsCard({ }} >
diff --git a/app/components/landing-page/ContributeCard.tsx b/app/components/landing-page/ContributeCard.tsx index 60b3e6b..36147f3 100644 --- a/app/components/landing-page/ContributeCard.tsx +++ b/app/components/landing-page/ContributeCard.tsx @@ -22,7 +22,7 @@ export default function ContributeCard() { return (

Open Source diff --git a/app/components/landing-page/Contributors.tsx b/app/components/landing-page/Contributors.tsx index ed73f1c..c0ebc65 100644 --- a/app/components/landing-page/Contributors.tsx +++ b/app/components/landing-page/Contributors.tsx @@ -25,7 +25,7 @@ export default async function Contributors() { return (

@@ -46,7 +46,7 @@ export default async function Contributors() { href={contributor.html_url} target="_blank" rel="noopener noreferrer" - className="glass-card rounded-2xl border-gray-200 bg-gray-50 p-4 transition-all hover:bg-white/[0.035] hover:border-indigo-500/20" + className="glass-card rounded-2xl border-gray-200 bg-gray-50 p-4 transition-all hover:bg-white/[0.035] hover:border-blue-500/20" >
0 && (
-

+

Team Insight

@@ -40,7 +40,7 @@ export default function LosserMembers({ flow.

- + Under 4h tracked
@@ -53,7 +53,7 @@ export default function LosserMembers({
-

+

Management View

@@ -80,26 +80,26 @@ export default function LosserMembers({
- + #{i + 1} {member.email.split("@")[0]}
- + {formatDuration(member.total_seconds)}
diff --git a/app/components/landing-page/RecentLeaderboard.tsx b/app/components/landing-page/RecentLeaderboard.tsx index fa28083..23c0d75 100644 --- a/app/components/landing-page/RecentLeaderboard.tsx +++ b/app/components/landing-page/RecentLeaderboard.tsx @@ -20,11 +20,11 @@ export default function RecentLeaderboard({ {leaderboards && leaderboards.length > 0 && (
-

+

Board Arena

@@ -36,12 +36,12 @@ export default function RecentLeaderboard({

- + {leaderboards.length} active boards Create yours @@ -54,14 +54,14 @@ export default function RecentLeaderboard({ {featuredBoard && (
- + Featured Arena -

+

{featuredBoard.name}

@@ -73,7 +73,7 @@ export default function RecentLeaderboard({

Status

-

+

Live

@@ -87,7 +87,7 @@ export default function RecentLeaderboard({

Action

-

+

Enter

@@ -105,12 +105,12 @@ export default function RecentLeaderboard({
- + Arena #{i + 2} @@ -118,13 +118,13 @@ export default function RecentLeaderboard({
-

+

{board.name}

@@ -133,7 +133,7 @@ export default function RecentLeaderboard({ /leaderboard/{board.slug} - + Enter →
@@ -154,7 +154,7 @@ export default function RecentLeaderboard({
View all leaderboards diff --git a/app/components/landing-page/TopLeaderbord.tsx b/app/components/landing-page/TopLeaderbord.tsx index faaea40..efa5986 100644 --- a/app/components/landing-page/TopLeaderbord.tsx +++ b/app/components/landing-page/TopLeaderbord.tsx @@ -46,11 +46,11 @@ export default function TopLeaderboard({ {top_members && top_members.length > 0 && (
-

+

Real Leaderboard

@@ -61,7 +61,7 @@ export default function TopLeaderboard({ progress to the top.

-
+
{rankedTopMembers.length} ranked developers
@@ -93,7 +93,7 @@ export default function TopLeaderboard({
0 && (
-

+

AI Category

@@ -32,7 +32,7 @@ export default function VibeCoders({ Snapshot of developers investing the most time in AI Coding.

- + AI coding signal
@@ -43,7 +43,7 @@ export default function VibeCoders({ )}
-

+

Category Focus

@@ -64,26 +64,26 @@ export default function VibeCoders({
- + #{i + 1} {member.email.split("@")[0]}
- + {formatDuration(member.total_seconds)}
Sign up diff --git a/app/components/leaderboard/BackButton.tsx b/app/components/leaderboard/BackButton.tsx index 5a8c7a2..c09679e 100644 --- a/app/components/leaderboard/BackButton.tsx +++ b/app/components/leaderboard/BackButton.tsx @@ -14,9 +14,9 @@ export default function BackButton({ return ( @@ -520,7 +520,7 @@ export default function Kanban() { {currentProject ? (
@@ -725,7 +725,7 @@ export default function Kanban() { onChange={(event) => setProjectForm((prev) => ({ ...prev, - wakatimeProjectName: event.target.value, + wakatimeproject_name: event.target.value, })) } className="input-field w-full" @@ -970,7 +970,7 @@ function Column({
diff --git a/app/d/settings/page.tsx b/app/d/settings/page.tsx index 67e13f1..87c2f1d 100644 --- a/app/d/settings/page.tsx +++ b/app/d/settings/page.tsx @@ -13,14 +13,14 @@ export default async function SettingsPage() { const { user } = await getCurrentUser(); if (!user) return redirect("/login?from=/settings"); - const hasWakaKey = Boolean(user.wakatimeApiKey); - const maskedWakaKey = user.wakatimeApiKey - ? `${user.wakatimeApiKey.slice(0, 8)}...${user.wakatimeApiKey.slice(-4)}` + const hasWakaKey = Boolean(user.wakatime_api_key); + const maskedWakaKey = user.wakatime_api_key + ? `${user.wakatime_api_key.slice(0, 8)}...${user.wakatime_api_key.slice(-4)}` : null; return (
-
+

diff --git a/app/internal-server-error.tsx b/app/internal-server-error.tsx index d508306..16e8f7d 100644 --- a/app/internal-server-error.tsx +++ b/app/internal-server-error.tsx @@ -36,14 +36,14 @@ export default function InternalServerError() {
42h 15m
Last 7 days
-
+
{/* Card 2 */}

@@ -88,7 +88,7 @@ export default function InternalServerError() { {/* Card 3 (Code terminal) */}
diff --git a/app/lib/auth.ts b/app/lib/auth.ts index dfab02b..16207bf 100644 --- a/app/lib/auth.ts +++ b/app/lib/auth.ts @@ -73,17 +73,17 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ if ( token.sub && (typeof token.role !== "string" || - !token.emailVerified || - !token.wakatimeApiKey) + !token.email_verified || + !token.wakatime_api_key) ) { const dbUser = await prisma.user.findUnique({ where: { id: token.sub }, - select: { role: true, emailVerified: true, wakatimeApiKey: true }, + select: { role: true, email_verified: true, wakatime_api_key: true }, }); if (dbUser) { token.role = dbUser.role; - token.emailVerified = dbUser.emailVerified; - token.wakatimeApiKey = dbUser.wakatimeApiKey; + token.email_verified = dbUser.email_verified; + token.wakatime_api_key = dbUser.wakatime_api_key; } } @@ -94,23 +94,23 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ session.user.id = token.sub; } session.user.role = typeof token.role === "string" ? token.role : "user"; - session.user.emailVerified = - (token.emailVerified as Date | null | undefined) ?? null; - session.user.wakatimeApiKey = token.wakatimeApiKey; + session.user.email_verified = + (token.email_verified as Date | null | undefined) ?? null; + session.user.wakatime_api_key = token.wakatime_api_key; return session; }, async signIn({ user, account }) { if (!user.id) return true; await prisma.userStats.upsert({ - where: { userId: user.id }, - create: { userId: user.id }, + where: { user_id: user.id }, + create: { user_id: user.id }, update: {}, }); await prisma.userProjects.upsert({ - where: { userId: user.id }, - create: { userId: user.id }, + where: { user_id: user.id }, + create: { user_id: user.id }, update: {}, }); @@ -124,14 +124,14 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ await prisma.conversationParticipant.upsert({ where: { - conversationId_userId: { - conversationId: globalConversationId, - userId: user.id, + conversation_id_user_id: { + conversation_id: globalConversationId, + user_id: user.id, }, }, create: { - conversationId: globalConversationId, - userId: user.id, + conversation_id: globalConversationId, + user_id: user.id, email: user.email, type: "global", }, diff --git a/app/lib/auth/user.ts b/app/lib/auth/user.ts index d4c4d44..ad51c69 100644 --- a/app/lib/auth/user.ts +++ b/app/lib/auth/user.ts @@ -14,7 +14,7 @@ export const getCurrentUser = cache(async () => { name: true, image: true, role: true, - wakatimeApiKey: true, + wakatime_api_key: true, }, }); diff --git a/app/lib/kanban.ts b/app/lib/kanban.ts index a9de4a8..3cc7256 100644 --- a/app/lib/kanban.ts +++ b/app/lib/kanban.ts @@ -62,7 +62,7 @@ function normalizeWakaTimeProjects(value: unknown): WakaTimeProject[] { } export async function getKanbanProjectAccess( - userId: string, + user_id: string, projectId: string, ): Promise { const rows = await prisma.$queryRaw>` @@ -77,13 +77,13 @@ export async function getKanbanProjectAccess( } export async function getColumnAccess( - userId: string, + user_id: string, columnId: string, ): Promise< | { columnId: string; projectId: string; - projectName: string; + project_name: string; } | null > { @@ -91,7 +91,7 @@ export async function getColumnAccess( Array<{ columnId: string; projectId: string; - projectName: string; + project_name: string; }> >` SELECT @@ -110,7 +110,7 @@ export async function getColumnAccess( } export async function getIssueAccess( - userId: string, + user_id: string, issueId: string, ): Promise< | { @@ -140,7 +140,7 @@ export async function getIssueAccess( return rows[0] ?? null; } -export async function getNextIssueKey(projectId: string, projectName: string) { +export async function getNextIssueKey(projectId: string, project_name: string) { const rows = await prisma.$queryRaw>` SELECT COUNT(*) AS issueCount FROM issues i @@ -164,7 +164,7 @@ export async function getNextIssueKey(projectId: string, projectName: string) { return `${prefix}-${String(issueCount + 1).padStart(3, "0")}`; } -export async function getKanbanData(userId: string) { +export async function getKanbanData(user_id: string) { const [projectRows, boardRows, columnRows, issueRows, userProjects] = await Promise.all([ prisma.$queryRaw` @@ -246,7 +246,7 @@ export async function getKanbanData(userId: string) { name: project.name, description: project.description ?? "", wakatime_project_name: project.wakatime_project_name ?? "", - color: project.color ?? "indigo", + color: project.color ?? "blue", created_at: project.created_at.toISOString(), board_count: boards.length, column_count: columns.length, diff --git a/app/lib/proxy/auth.ts b/app/lib/proxy/auth.ts index f4573ca..b3c5e0b 100644 --- a/app/lib/proxy/auth.ts +++ b/app/lib/proxy/auth.ts @@ -14,10 +14,10 @@ export default async function Auth(req: NextRequest) { if (!session) { return NextResponse.redirect(new URL("/login", req.url)); } - if (!session.user.emailVerified) { + if (!session.user.email_verified) { return NextResponse.redirect(new URL("/verify-email", req.url)); } - if (!session.user.wakatimeApiKey && pathname.startsWith("/d")) { + if (!session.user.wakatime_api_key && pathname.startsWith("/d")) { return NextResponse.redirect(new URL("/verify-wakatime", req.url)); } } @@ -30,18 +30,18 @@ export default async function Auth(req: NextRequest) { ]; if (authRoutes.includes(pathname) && session) { - if (!session.user.emailVerified) { + if (!session.user.email_verified) { return NextResponse.redirect(new URL("/verify-email", req.url)); } - if (!session.user.wakatimeApiKey) { + if (!session.user.wakatime_api_key) { return NextResponse.redirect(new URL("/verify-wakatime", req.url)); } return NextResponse.redirect(new URL("/d", req.url)); } if ( - (pathname === "/verify-email" && session?.user.emailVerified) || - (pathname === "/verify-wakatime" && session?.user.wakatimeApiKey) + (pathname === "/verify-email" && session?.user.email_verified) || + (pathname === "/verify-wakatime" && session?.user.wakatime_api_key) ) { return NextResponse.redirect(new URL("/d", req.url)); } diff --git a/app/lib/wakatime/repository.ts b/app/lib/wakatime/repository.ts index 60c2234..71467d8 100644 --- a/app/lib/wakatime/repository.ts +++ b/app/lib/wakatime/repository.ts @@ -4,12 +4,16 @@ import type { Prisma } from "@prisma/client"; /** * Fetches user's coding stats along with their project list. */ -export async function getExistingUserStats(userId: string) { +export async function getExistingUserStats(user_id: string) { const stats = await prisma.userStats.findUnique({ - where: { userId }, - include: { user: { select: { userProjects: true } } }, + where: { user_id }, + include: { user: { select: { user_projects: true } } }, }); - return stats; + + return { + ...stats, + projects: stats?.user.user_projects?.projects, + }; } /** @@ -17,12 +21,12 @@ export async function getExistingUserStats(userId: string) { * Throws Prisma P2002 if the key is already used by another account. */ export async function updateProfileWakatimeApiKey( - userId: string, + user_id: string, apiKey: string, ) { return prisma.user.update({ - where: { id: userId }, - data: { wakatimeApiKey: apiKey }, + where: { id: user_id }, + data: { wakatime_api_key: apiKey }, }); } @@ -33,7 +37,7 @@ export async function upsertUserStats( payload: Prisma.UserStatsUncheckedCreateInput, ) { return prisma.userStats.upsert({ - where: { userId: payload.userId }, + where: { user_id: payload.user_id }, create: payload, update: payload, }); @@ -46,7 +50,7 @@ export async function upsertUserProjects( payload: Prisma.UserProjectsUncheckedCreateInput, ) { return prisma.userProjects.upsert({ - where: { userId: payload.userId }, + where: { user_id: payload.user_id }, create: payload, update: payload, }); @@ -60,9 +64,9 @@ export async function upsertUserDashboardSnapshot( ) { return prisma.userDashboardSnapshot.upsert({ where: { - userId_snapshotDate: { - userId: payload.userId, - snapshotDate: payload.snapshotDate as Date, + user_id_snapshot_date: { + user_id: payload.user_id, + snapshot_date: payload.snapshot_date as Date, }, }, create: payload, diff --git a/app/lib/wakatime/sync.ts b/app/lib/wakatime/sync.ts index 7d1243f..fc59469 100644 --- a/app/lib/wakatime/sync.ts +++ b/app/lib/wakatime/sync.ts @@ -169,12 +169,12 @@ export async function syncWakatimeData({ if (!normalizedIncomingApiKey) { const existing = await getExistingUserStats(userId); - const existingDailyStats = Array.isArray(existing?.dailyStats) - ? existing.dailyStats + const existingDailyStats = Array.isArray(existing?.daily_stats) + ? existing.daily_stats : []; - if (existing?.lastFetchedAt) { - const lastFetch = new Date(existing.lastFetchedAt).getTime(); + if (existing?.last_fetched_at) { + const lastFetch = new Date(existing.last_fetched_at).getTime(); if ( Date.now() - lastFetch < SIX_HOURS_MS && (existingDailyStats as unknown[]).length >= CONSISTENCY_DAYS @@ -232,24 +232,24 @@ export async function syncWakatimeData({ const [statsResult, projectsResult] = await Promise.all([ upsertUserStats({ - userId, - totalSeconds: BigInt(Math.floor(waka.stats.total_seconds || 0)), - dailyAverage: BigInt(Math.floor(waka.stats.daily_average || 0)), + user_id: userId, + total_seconds: BigInt(Math.floor(waka.stats.total_seconds || 0)), + daily_average: BigInt(Math.floor(waka.stats.daily_average || 0)), languages: (waka.stats.languages || []) as Prisma.InputJsonValue, - operatingSystems: (waka.stats.operating_systems || + operating_systems: (waka.stats.operating_systems || []) as Prisma.InputJsonValue, editors: (waka.stats.editors || []) as Prisma.InputJsonValue, machines: (waka.stats.machines || []) as Prisma.InputJsonValue, categories: (waka.stats.categories || []) as Prisma.InputJsonValue, dependencies: (waka.stats.dependencies || []) as Prisma.InputJsonValue, - bestDay: (waka.stats.best_day || {}) as Prisma.InputJsonValue, - dailyStats: dailyStats as unknown as Prisma.InputJsonValue, - lastFetchedAt: new Date(nowIso), + best_day: (waka.stats.best_day || {}) as Prisma.InputJsonValue, + daily_stats: dailyStats as unknown as Prisma.InputJsonValue, + last_fetched_at: new Date(nowIso), }), upsertUserProjects({ - userId, + user_id: userId, projects: (waka.stats.projects || []) as Prisma.InputJsonValue, - lastFetchedAt: new Date(nowIso), + last_fetched_at: new Date(nowIso), }), ]); @@ -260,23 +260,23 @@ export async function syncWakatimeData({ try { await upsertUserDashboardSnapshot({ - userId, - snapshotDate: new Date(endStr), - totalSeconds7d: BigInt(snapshotMetrics.totalSeconds7d), - activeDays7d: snapshotMetrics.activeDays7d, - consistencyPercent: snapshotMetrics.consistencyPercent, - currentStreak: snapshotMetrics.currentStreak, - bestStreak: snapshotMetrics.bestStreak, - peakDay: snapshotMetrics.peakDayDate + user_id: userId, + snapshot_date: new Date(endStr), + total_seconds_7d: BigInt(snapshotMetrics.totalSeconds7d), + active_days_7d: snapshotMetrics.activeDays7d, + consistency_percent: snapshotMetrics.consistencyPercent, + current_streak: snapshotMetrics.currentStreak, + best_streak: snapshotMetrics.bestStreak, + peak_day: snapshotMetrics.peakDayDate ? new Date(snapshotMetrics.peakDayDate) : null, - peakDaySeconds: BigInt(snapshotMetrics.peakDaySeconds), - topLanguage: topLanguage?.name || null, - topLanguagePercent: + peak_day_seconds: BigInt(snapshotMetrics.peakDaySeconds), + top_language: topLanguage?.name || null, + top_language_percent: typeof topLanguage?.percent === "number" ? new Prisma.Decimal(topLanguage.percent.toFixed(2)) : null, - updatedAt: new Date(nowIso), + updated_at: new Date(nowIso), }); } catch (err) { console.error("Failed to upsert user dashboard snapshot", err); diff --git a/app/not-found.tsx b/app/not-found.tsx index 7e27d84..7528c2c 100644 --- a/app/not-found.tsx +++ b/app/not-found.tsx @@ -34,14 +34,14 @@ export default function NotFound() {
42h 15m
Last 7 days
-
+
{/* Card 2 */}

@@ -86,7 +86,7 @@ export default function NotFound() { {/* Card 3 (Code terminal) */}
diff --git a/app/page.tsx b/app/page.tsx index 488a978..fde4032 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -35,37 +35,37 @@ export default async function Home() { const [leaderboards, losserStatsRows, topStatsRows] = await Promise.all([ prisma.leaderboard.findMany({ select: { id: true, name: true, slug: true }, - orderBy: { createdAt: "desc" }, + orderBy: { created_at: "desc" }, take: 5, }), prisma.userStats.findMany({ - where: { totalSeconds: { gt: 0, lt: 14400 } }, + where: { total_seconds: { gt: 0, lt: 14400 } }, select: { - userId: true, - totalSeconds: true, + user_id: true, + total_seconds: true, categories: true, user: { select: { email: true } }, }, - orderBy: { totalSeconds: "asc" }, + orderBy: { total_seconds: "asc" }, take: 50, }), prisma.userStats.findMany({ - where: { totalSeconds: { gt: 0 } }, + where: { total_seconds: { gt: 0 } }, select: { - userId: true, - totalSeconds: true, + user_id: true, + total_seconds: true, categories: true, user: { select: { email: true } }, }, - orderBy: { totalSeconds: "desc" }, + orderBy: { total_seconds: "desc" }, take: 50, }), ]); const toMember = (row: (typeof topStatsRows)[number]): RawMember => ({ - user_id: row.userId, + user_id: row.user_id, email: row.user.email, - total_seconds: Number(row.totalSeconds), + total_seconds: Number(row.total_seconds), categories: row.categories as MemberCategory[] | null, }); @@ -157,7 +157,7 @@ export default async function Home() {

Measure your
@@ -166,7 +166,7 @@ export default async function Home() {

Turn your daily coding activity into competitive, shareable @@ -176,7 +176,7 @@ export default async function Home() {

Last 7 days
-
+
{/* Card 2 */} -
+

Top Languages @@ -306,13 +306,13 @@ export default async function Home() {

Everything you need to grow.

Devpulse integrates seamlessly with your tools to provide @@ -325,7 +325,7 @@ export default async function Home() { icon={ } title="Private & Public Boards" @@ -383,11 +383,11 @@ function FeatureCard({ }) { return (

-
+
{icon}

{title}

diff --git a/app/sitemaps/leaderboards.ts b/app/sitemaps/leaderboards.ts index 9e5d9df..ed7875f 100644 --- a/app/sitemaps/leaderboards.ts +++ b/app/sitemaps/leaderboards.ts @@ -4,7 +4,7 @@ import { prisma } from "../lib/prisma"; export default async function sitemap(): Promise { const leaderboards = await prisma.leaderboard.findMany({ select: { slug: true }, - orderBy: { createdAt: "desc" }, + orderBy: { created_at: "desc" }, }); return leaderboards.map((lb) => ({ diff --git a/app/utils/time.ts b/app/utils/time.ts index d32d552..837fc86 100644 --- a/app/utils/time.ts +++ b/app/utils/time.ts @@ -1,12 +1,33 @@ -export function formatHours(seconds: number) { - const safeSeconds = Number.isFinite(seconds) ? Math.max(0, seconds) : 0; - const totalMinutes = Math.round(safeSeconds / 60); - const hrs = Math.floor(totalMinutes / 60); - const mins = totalMinutes % 60; - if (hrs > 0) return `${hrs}h ${mins}m`; - return `${mins}m`; +/** + * Formats the given number of seconds as a human-readable string representing the number of hours and minutes. + * + * @param seconds The number/string of seconds to format. + * @returns A human-readable string representing the number of hours and minutes, or `null` if the input is invalid. + */ +export function formatHours(seconds: string | number) { + try { + const safeSeconds = Number(seconds); + if (!Number.isFinite(safeSeconds)) return null; + + const totalMinutes = Math.ceil(safeSeconds / 60); + + const hrs = Math.floor(totalMinutes / 60); + const mins = totalMinutes % 60; + + if (hrs > 0) return `${hrs}h ${mins}m`; + return `${mins}m`; + } catch { + console.error("Invalid seconds:", seconds); + return null; + } } +/** + * Returns a human-readable string representing the time elapsed since the given timestamp. + * + * @param timestamp The timestamp to compare against the current time. + * @returns A human-readable string representing the time elapsed since the given timestamp, or `null` if the timestamp is invalid. + */ export function timeAgo(timestamp: string) { if (!timestamp) return null; diff --git a/next-auth.d.ts b/next-auth.d.ts index abb91d9..e2ea773 100644 --- a/next-auth.d.ts +++ b/next-auth.d.ts @@ -6,11 +6,11 @@ declare module "next-auth" { user: { id: string; role: string; - emailVerified: Date | null; + email_verified: Date | null; email?: string | null; name?: string | null; image?: string | null; - wakatimeApiKey?: string | null; + wakatime_api_key?: string | null; }; } @@ -24,7 +24,7 @@ declare module "next-auth/jwt" { interface JWT { id?: string; role?: string; - emailVerified?: Date | null; - wakatimeApiKey?: string | null; + email_verified?: Date | null; + wakatime_api_key?: string | null; } } diff --git a/prisma/migrations/20260728050901_kanban/migration.sql b/prisma/migrations/20260728050901_kanban/migration.sql deleted file mode 100644 index 80912a7..0000000 --- a/prisma/migrations/20260728050901_kanban/migration.sql +++ /dev/null @@ -1,5 +0,0 @@ --- AlterTable -ALTER TABLE `conversation_participants` MODIFY `last_seen_at` DATETIME(3) NOT NULL DEFAULT '1970-01-01 00:00:00'; - --- AlterTable -ALTER TABLE `messages` MODIFY `expires_at` DATETIME(3) NOT NULL DEFAULT (NOW() + INTERVAL 30 DAY); diff --git a/prisma/migrations/20260728120000_extend_kanban_projects/migration.sql b/prisma/migrations/20260728120000_extend_kanban_projects/migration.sql deleted file mode 100644 index fce37c9..0000000 --- a/prisma/migrations/20260728120000_extend_kanban_projects/migration.sql +++ /dev/null @@ -1,13 +0,0 @@ -ALTER TABLE `projects` - ADD COLUMN `user_id` VARCHAR(191) NULL, - ADD COLUMN `description` VARCHAR(191) NULL, - ADD COLUMN `wakatime_project_name` VARCHAR(191) NULL, - ADD COLUMN `color` VARCHAR(32) NULL DEFAULT 'indigo'; - -CREATE INDEX `projects_user_id_idx` ON `projects`(`user_id`); -CREATE INDEX `projects_wakatime_project_name_idx` ON `projects`(`wakatime_project_name`); - -ALTER TABLE `projects` - ADD CONSTRAINT `projects_user_id_fkey` - FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) - ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20260728043534_init/migration.sql b/prisma/migrations/20260822201501_init/migration.sql similarity index 96% rename from prisma/migrations/20260728043534_init/migration.sql rename to prisma/migrations/20260822201501_init/migration.sql index 2bb5c57..5716f97 100644 --- a/prisma/migrations/20260728043534_init/migration.sql +++ b/prisma/migrations/20260822201501_init/migration.sql @@ -3,7 +3,7 @@ CREATE TABLE `users` ( `id` VARCHAR(191) NOT NULL, `name` VARCHAR(191) NULL, `email` VARCHAR(191) NOT NULL, - `emailVerified` DATETIME(3) NULL, + `email_verified` DATETIME(3) NULL, `image` VARCHAR(191) NULL, `password` VARCHAR(191) NULL, `role` VARCHAR(191) NOT NULL DEFAULT 'user', @@ -199,9 +199,14 @@ CREATE TABLE `messages` ( -- CreateTable CREATE TABLE `projects` ( `id` VARCHAR(191) NOT NULL, + `user_id` VARCHAR(191) NULL, `name` VARCHAR(191) NOT NULL, + `description` VARCHAR(191) NULL, + `wakatime_project_name` VARCHAR(191) NULL, + `color` VARCHAR(191) NULL DEFAULT 'blue', `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + INDEX `projects_user_id_idx`(`user_id`), PRIMARY KEY (`id`) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; @@ -289,6 +294,9 @@ ALTER TABLE `messages` ADD CONSTRAINT `messages_conversation_id_fkey` FOREIGN KE -- AddForeignKey ALTER TABLE `messages` ADD CONSTRAINT `messages_sender_id_fkey` FOREIGN KEY (`sender_id`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; +-- AddForeignKey +ALTER TABLE `projects` ADD CONSTRAINT `projects_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + -- AddForeignKey ALTER TABLE `boards` ADD CONSTRAINT `boards_project_id_fkey` FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a4ee790..39e1c17 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,59 +8,59 @@ datasource db { } model User { - id String @id @default(cuid()) - name String? - email String @unique - emailVerified DateTime? - image String? - password String? - role String @default("user") - wakatimeApiKey String? @unique @map("wakatime_api_key") - createdAt DateTime @default(now()) @map("created_at") - - accounts Account[] - sessions Session[] - userStats UserStats? - userProjects UserProjects? - dashboardSnapshots UserDashboardSnapshot[] - userFlexes UserFlex[] - ownedLeaderboards Leaderboard[] - leaderboardMemberships LeaderboardMember[] - conversationParticipants ConversationParticipant[] - sentMessages Message[] - passwordResetTokens PasswordResetToken[] - kanbanProjects Project[] + id String @id @default(cuid()) + name String? + email String @unique + email_verified DateTime? + image String? + password String? + role String @default("user") + wakatime_api_key String? @unique + created_at DateTime @default(now()) + + accounts Account[] + sessions Session[] + user_stats UserStats? + user_projects UserProjects? + dashboard_snapshots UserDashboardSnapshot[] + user_flexes UserFlex[] + owned_leaderboards Leaderboard[] + leaderboard_memberships LeaderboardMember[] + conversation_participants ConversationParticipant[] + sent_messages Message[] + password_reset_tokens PasswordResetToken[] + kanban_projects Project[] @@map("users") } model Account { - id String @id @default(cuid()) - userId String @map("user_id") - type String - provider String - providerAccountId String @map("provider_account_id") - refresh_token String? @db.Text - access_token String? @db.Text - expires_at Int? - token_type String? - scope String? - id_token String? @db.Text - session_state String? - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@unique([provider, providerAccountId]) + id String @id @default(cuid()) + user_id String + type String + provider String + provider_account_id String + refresh_token String? @db.Text + access_token String? @db.Text + expires_at Int? + token_type String? + scope String? + id_token String? @db.Text + session_state String? + + user User @relation(fields: [user_id], references: [id], onDelete: Cascade) + + @@unique([provider, provider_account_id]) @@map("accounts") } model Session { - id String @id @default(cuid()) - sessionToken String @unique @map("session_token") - userId String @map("user_id") - expires DateTime + id String @id @default(cuid()) + session_token String @unique + user_id String + expires DateTime - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + user User @relation(fields: [user_id], references: [id], onDelete: Cascade) @@map("sessions") } @@ -75,82 +75,82 @@ model VerificationToken { } model PasswordResetToken { - id String @id @default(cuid()) - token String @unique @default(cuid()) - userId String @map("user_id") - expiresAt DateTime @map("expires_at") - createdAt DateTime @default(now()) @map("created_at") + id String @id @default(cuid()) + token String @unique @default(cuid()) + user_id String + expires_at DateTime + created_at DateTime @default(now()) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + user User @relation(fields: [user_id], references: [id], onDelete: Cascade) @@map("password_reset_tokens") } model UserStats { - userId String @id @map("user_id") - totalSeconds BigInt @default(0) @map("total_seconds") - dailyAverage BigInt @default(0) @map("daily_average") - languages Json @default("[]") - operatingSystems Json @default("[]") @map("operating_systems") - editors Json @default("[]") - machines Json @default("[]") - categories Json @default("[]") - dependencies Json @default("[]") - bestDay Json @default("{}") @map("best_day") - dailyStats Json @default("[]") @map("daily_stats") - lastFetchedAt DateTime @default(now()) @map("last_fetched_at") - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + user_id String @id + total_seconds BigInt @default(0) + daily_average BigInt @default(0) + languages Json @default("[]") + operating_systems Json @default("[]") + editors Json @default("[]") + machines Json @default("[]") + categories Json @default("[]") + dependencies Json @default("[]") + best_day Json @default("{}") + daily_stats Json @default("[]") + last_fetched_at DateTime @default(now()) + + user User @relation(fields: [user_id], references: [id], onDelete: Cascade) @@map("user_stats") } model UserProjects { - userId String @id @map("user_id") - projects Json @default("[]") - lastFetchedAt DateTime @default(now()) @map("last_fetched_at") + user_id String @id + projects Json @default("[]") + last_fetched_at DateTime @default(now()) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + user User @relation(fields: [user_id], references: [id], onDelete: Cascade) @@map("user_projects") } model UserDashboardSnapshot { - id Int @id @default(autoincrement()) - userId String @map("user_id") - snapshotDate DateTime @map("snapshot_date") @db.Date - totalSeconds7d BigInt @default(0) @map("total_seconds_7d") - activeDays7d Int @default(0) @map("active_days_7d") - consistencyPercent Int @default(0) @map("consistency_percent") - currentStreak Int @default(0) @map("current_streak") - bestStreak Int @default(0) @map("best_streak") - peakDay DateTime? @map("peak_day") @db.Date - peakDaySeconds BigInt @default(0) @map("peak_day_seconds") - topLanguage String? @map("top_language") - topLanguagePercent Decimal? @map("top_language_percent") @db.Decimal(5, 2) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @default(now()) @map("updated_at") - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@unique([userId, snapshotDate]) + id Int @id @default(autoincrement()) + user_id String + snapshot_date DateTime @db.Date + total_seconds_7d BigInt @default(0) + active_days_7d Int @default(0) + consistency_percent Int @default(0) + current_streak Int @default(0) + best_streak Int @default(0) + peak_day DateTime? @db.Date + peak_day_seconds BigInt @default(0) + top_language String? + top_language_percent Decimal? @db.Decimal(5, 2) + created_at DateTime @default(now()) + updated_at DateTime @default(now()) + + user User @relation(fields: [user_id], references: [id], onDelete: Cascade) + + @@unique([user_id, snapshot_date]) @@map("user_dashboard_snapshots") } model UserFlex { - id String @id @default(cuid()) - userId String @map("user_id") - userEmail String @map("user_email") - projectName String @map("project_name") - projectDescription String @map("project_description") @db.Text - projectUrl String @map("project_url") - projectTime String @map("project_time") - isOpenSource Boolean @default(false) @map("is_open_source") - openSourceUrl String @default("") @map("open_source_url") - createdAt DateTime @default(now()) @map("created_at") - expiresAt DateTime @map("expires_at") - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + user_id String + user_email String + project_name String + project_description String @db.Text + project_url String + project_time String + is_open_source Boolean @default(false) + open_source_url String @default("") + created_at DateTime @default(now()) + expires_at DateTime + + user User @relation(fields: [user_id], references: [id], onDelete: Cascade) @@map("user_flexes") } @@ -160,35 +160,35 @@ model Leaderboard { name String slug String @unique description String? - isPublic Boolean @default(true) @map("is_public") - ownerId String @map("owner_id") - joinCode String @unique @map("join_code") - createdAt DateTime @default(now()) @map("created_at") + is_public Boolean @default(true) + owner_id String + join_code String @unique + created_at DateTime @default(now()) - owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) + owner User @relation(fields: [owner_id], references: [id], onDelete: Cascade) members LeaderboardMember[] @@map("leaderboards") } model LeaderboardMember { - id String @id @default(cuid()) - leaderboardId String @map("leaderboard_id") - userId String @map("user_id") - role String @default("member") - joinedAt DateTime @default(now()) @map("joined_at") + id String @id @default(cuid()) + leaderboard_id String + user_id String + role String @default("member") + joined_at DateTime @default(now()) - leaderboard Leaderboard @relation(fields: [leaderboardId], references: [id], onDelete: Cascade) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + leaderboard Leaderboard @relation(fields: [leaderboard_id], references: [id], onDelete: Cascade) + user User @relation(fields: [user_id], references: [id], onDelete: Cascade) - @@unique([leaderboardId, userId]) + @@unique([leaderboard_id, user_id]) @@map("leaderboard_members") } model Conversation { - id String @id @default(cuid()) - type ConversationType @default(PRIVATE) - createdAt DateTime @default(now()) @map("created_at") + id String @id @default(cuid()) + type ConversationType @default(PRIVATE) + created_at DateTime @default(now()) participants ConversationParticipant[] messages Message[] @@ -202,96 +202,96 @@ enum ConversationType { } model ConversationParticipant { - conversationId String @map("conversation_id") - userId String @map("user_id") - email String - type String @default("private") - lastSeenAt DateTime @default(dbgenerated("'1970-01-01 00:00:00'")) @map("last_seen_at") - lastReadAt DateTime @default(now()) @map("last_read_at") - - conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@id([conversationId, userId]) - @@index([userId, lastSeenAt(sort: Desc)]) + conversation_id String + user_id String + email String + type String @default("private") + last_seen_at DateTime @default(dbgenerated("'1970-01-01 00:00:00'")) + last_read_at DateTime @default(now()) + + conversation Conversation @relation(fields: [conversation_id], references: [id], onDelete: Cascade) + user User @relation(fields: [user_id], references: [id], onDelete: Cascade) + + @@id([conversation_id, user_id]) + @@index([user_id, last_seen_at(sort: Desc)]) @@map("conversation_participants") } model Message { - id String @id @default(cuid()) - conversationId String @map("conversation_id") - senderId String @map("sender_id") - text String @db.Text - attachments Json @default("[]") - createdAt DateTime @default(now()) @map("created_at") - expiresAt DateTime @default(dbgenerated("(NOW() + INTERVAL 30 DAY)")) @map("expires_at") - - conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade) - sender User @relation(fields: [senderId], references: [id]) - - @@index([conversationId, createdAt(sort: Desc), senderId]) + id String @id @default(cuid()) + conversation_id String + sender_id String + text String @db.Text + attachments Json @default("[]") + created_at DateTime @default(now()) + expires_at DateTime @default(dbgenerated("(NOW() + INTERVAL 30 DAY)")) + + conversation Conversation @relation(fields: [conversation_id], references: [id], onDelete: Cascade) + sender User @relation(fields: [sender_id], references: [id]) + + @@index([conversation_id, created_at(sort: Desc), sender_id]) @@map("messages") } model Project { - id String @id @default(cuid()) - userId String? @map("user_id") - name String - description String? - wakatimeProjectName String? @map("wakatime_project_name") - color String? @default("indigo") - createdAt DateTime @default(now()) @map("created_at") - - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + user_id String? + name String + description String? + wakatime_project_name String? + color String? @default("blue") + created_at DateTime @default(now()) + + user User? @relation(fields: [user_id], references: [id], onDelete: Cascade) boards Board[] - @@index([userId]) + @@index([user_id]) @@map("projects") } model Board { id String @id @default(cuid()) - projectId String @map("project_id") + project_id String title String description String? - createdAt DateTime @default(now()) @map("created_at") + created_at DateTime @default(now()) - project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + project Project @relation(fields: [project_id], references: [id], onDelete: Cascade) columns Column[] @@map("boards") } model Column { - id String @id @default(cuid()) - boardId String @map("board_id") - title String - position Int @default(0) - createdAt DateTime @default(now()) @map("created_at") + id String @id @default(cuid()) + board_id String + title String + position Int @default(0) + created_at DateTime @default(now()) - board Board @relation(fields: [boardId], references: [id], onDelete: Cascade) + board Board @relation(fields: [board_id], references: [id], onDelete: Cascade) issues Issue[] - @@index([boardId]) + @@index([board_id]) @@map("columns") } model Issue { - id String @id @default(cuid()) - columnId String @map("column_id") - issueKey String @unique @map("issue_key") - title String - tag String? - type IssueType @default(FEATURE) - priority IssuePriority @default(P2) - position Int @default(0) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @default(now()) @map("updated_at") - - column Column @relation(fields: [columnId], references: [id], onDelete: Cascade) - - @@index([columnId]) - @@index([columnId, position]) + id String @id @default(cuid()) + column_id String + issue_key String @unique + title String + tag String? + type IssueType @default(FEATURE) + priority IssuePriority @default(P2) + position Int @default(0) + created_at DateTime @default(now()) + updated_at DateTime @default(now()) + + column Column @relation(fields: [column_id], references: [id], onDelete: Cascade) + + @@index([column_id]) + @@index([column_id, position]) @@map("issues") } From c4f99bfec637989ea49f36aba7738600fed421dd Mon Sep 17 00:00:00 2001 From: Melvin Jones Repol Date: Sun, 23 Aug 2026 05:25:35 +0800 Subject: [PATCH 04/10] deps: update nextjs and other dependencies --- package-lock.json | 136 +++++++++++++++++++++++----------------------- package.json | 2 +- 2 files changed, 70 insertions(+), 68 deletions(-) diff --git a/package-lock.json b/package-lock.json index bdaf41b..b576828 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "bcryptjs": "^3.0.3", "devtools-detector": "^2.0.25", "mysql2": "^3.23.1", - "next": "16.1.6", + "next": "^16.2.11", "next-auth": "^5.0.0-beta.32", "nextjs-toploader": "^3.9.17", "nodemailer": "^8.0.11", @@ -1346,9 +1346,9 @@ } }, "node_modules/@next/env": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz", - "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==", + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.11.tgz", + "integrity": "sha512-0do5A3BJ2gxWr0ZCMcD6BhW+e595jyxdTl3rXTS6lOtD8ektMiW6CO+EPwt1Eca1DBnm90r/7GdiKWBKxH++DA==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -1362,9 +1362,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz", - "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==", + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.11.tgz", + "integrity": "sha512-wryL4pjKmDwGv2ox6+GZDFxvmtSRLqApBR8kL1j4+vhB7Z5vJC/zAnXpiR9Xkfzl0AS8WLMnsuGV/UKI67/rrw==", "cpu": [ "arm64" ], @@ -1378,9 +1378,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz", - "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==", + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.11.tgz", + "integrity": "sha512-aZl2j4f/fLyjQvOhv0Oe9UaMAQHolYpKhctsoYzplSumKJKPUmgjcf6545aBtysLTcu994TREd0+pSgNE4ohmg==", "cpu": [ "x64" ], @@ -1394,9 +1394,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz", - "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==", + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.11.tgz", + "integrity": "sha512-5jEriyEnH/LWFy27L2ZG0XaLlyEJIjhsImEsiS9P563PKEVp2BVups/xfOucIrsvVntp11oNcZwjHvaDPYVB5g==", "cpu": [ "arm64" ], @@ -1413,9 +1413,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz", - "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==", + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.11.tgz", + "integrity": "sha512-eIjcpx2fnnFSSkZDbTxy74KnokUXDjfoLClpWelfgHLf621aTqswhwXQ7GkD5K5rplrS6LZ/Bj+mVuvzluBOEg==", "cpu": [ "arm64" ], @@ -1432,9 +1432,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz", - "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==", + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.11.tgz", + "integrity": "sha512-8WgzpaWMs46qJT9kiV47cje86L0x/Mu9t8/Gwj+pnbgW3rETVfCnaScPjlYUwNScpOozdcIMHWmAvuZJUonR2w==", "cpu": [ "x64" ], @@ -1451,9 +1451,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", - "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==", + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.11.tgz", + "integrity": "sha512-I3UgPds7G4ZYnTb/H+5GBGuUT2DhAk6j0mL6A4s63RjFs74wB2hOWP0vaxsK+3NJraExt3eYEPQ/UtT0x/64Nw==", "cpu": [ "x64" ], @@ -1470,9 +1470,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz", - "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==", + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.11.tgz", + "integrity": "sha512-n89CjtcThnjrwgJMAiI5xbqwLY51zvwC9tSlArmVndAJLYVl9T9UAdlkXTmZvE++idoXe8KdglQlhNRdUp1c6g==", "cpu": [ "arm64" ], @@ -1486,9 +1486,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz", - "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==", + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.11.tgz", + "integrity": "sha512-md8CLNggS1Dx9pUgApzps5uAf+N8GN9xywzmNx9vHAWo94HtBwCCqkSnhIrdfQe83Dhz8Lfo/20Nb1Zxal092w==", "cpu": [ "x64" ], @@ -2098,9 +2098,9 @@ } }, "node_modules/@types/d3-shape": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", - "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==", "license": "MIT", "dependencies": { "@types/d3-path": "*" @@ -3262,9 +3262,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.11.14", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.14.tgz", - "integrity": "sha512-JyJ954WzuIR8/FFzX0o5krdSTrBAkcCSRfWSleRsIHSWV+cZe2FI1PKggVkFke1hBldRs+LRxUczzE9iPmgZww==", + "version": "2.11.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.18.tgz", + "integrity": "sha512-1iEmLEYSiE1SeBoAfPo/Mnx3PzfzHUkDK61ASkCpuk3YXugYLH5DYK1SzqV55F8FMI6s0F+/tCP7Polz1QRjxw==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -4077,9 +4077,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.407", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.407.tgz", - "integrity": "sha512-4R8XgQOdfxexCd/u63lRm6wCHjECwI45MV9wxAs2ggtfWe2hwlo1ql97jKsju2IcJ+jFSTwBssyYoiWhh7mauQ==", + "version": "1.5.412", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.412.tgz", + "integrity": "sha512-z4rMe3esBzlzovKHj4gxJnsCGZRK5l4baUvm+gCGJBPE+gsyUMKsuU9tnEUtI1dOebXz1ytAPGjvXhmQ7rIPwA==", "dev": true, "license": "ISC" }, @@ -4322,14 +4322,15 @@ } }, "node_modules/es-toolkit": { - "version": "1.50.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz", - "integrity": "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==", + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.51.0.tgz", + "integrity": "sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw==", "license": "MIT", "workspaces": [ "docs", "benchmarks", - "tests/types" + "tests/types", + "tests/browser-compat" ] }, "node_modules/escalade": { @@ -4358,6 +4359,7 @@ "version": "9.39.5", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", "dependencies": { @@ -5280,9 +5282,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.2.tgz", - "integrity": "sha512-XpwZALwwl/BaKTAyC6+c5T8y6kCg2jk+XGqOVrKIQmW49pNypYLMRjCUXqa28tQgJlhS2RlzP7sc+Rx7W6qsfw==", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.3.tgz", + "integrity": "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==", "dev": true, "license": "MIT", "dependencies": { @@ -5678,9 +5680,9 @@ } }, "node_modules/immer": { - "version": "11.1.17", - "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.17.tgz", - "integrity": "sha512-8Vu44Y0MuMBlTQz/jQ8HEMYNq/bBqk87MnBwYR5mC8AthfhEXidZ5aT/oA/CUqboa8THKltnD9L3xyqhU/Sy1Q==", + "version": "11.1.18", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.18.tgz", + "integrity": "sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ==", "license": "MIT", "funding": { "type": "opencollective", @@ -6314,9 +6316,9 @@ } }, "node_modules/jose": { - "version": "6.2.9", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.9.tgz", - "integrity": "sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA==", + "version": "6.2.10", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.10.tgz", + "integrity": "sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -7538,9 +7540,9 @@ "license": "MIT" }, "node_modules/mysql2": { - "version": "3.23.3", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.23.3.tgz", - "integrity": "sha512-ehp9HEKr4wVJaBOUVxNFa+CNrsCCCZ6363/jbGhb7WpEmSRNIXjHBjFs5K2s2cXn7j/RhDieejsQJ6nfUwD6vQ==", + "version": "3.23.4", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.23.4.tgz", + "integrity": "sha512-J1Rgl8Oy5iw3mOBjKeTMQ3cJNjMYtJYavQVXShsMtSj9rKV8Q1+QaGKNJqDVQFcNINqYYp6Vxd90r69cCoBxBA==", "license": "MIT", "dependencies": { "aws-ssl-profiles": "^1.1.2", @@ -7612,14 +7614,14 @@ "license": "MIT" }, "node_modules/next": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", - "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.11.tgz", + "integrity": "sha512-B339zaqbyK8cmxhoAvLrcwoabwCP1wz21zSzfqxqXAemTu2BXnH7tQnfcglKv1vnMUIDBc+Hth7XODQriTZiRQ==", "license": "MIT", "dependencies": { - "@next/env": "16.1.6", + "@next/env": "16.2.11", "@swc/helpers": "0.5.15", - "baseline-browser-mapping": "^2.8.3", + "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -7631,15 +7633,15 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.1.6", - "@next/swc-darwin-x64": "16.1.6", - "@next/swc-linux-arm64-gnu": "16.1.6", - "@next/swc-linux-arm64-musl": "16.1.6", - "@next/swc-linux-x64-gnu": "16.1.6", - "@next/swc-linux-x64-musl": "16.1.6", - "@next/swc-win32-arm64-msvc": "16.1.6", - "@next/swc-win32-x64-msvc": "16.1.6", - "sharp": "^0.34.4" + "@next/swc-darwin-arm64": "16.2.11", + "@next/swc-darwin-x64": "16.2.11", + "@next/swc-linux-arm64-gnu": "16.2.11", + "@next/swc-linux-arm64-musl": "16.2.11", + "@next/swc-linux-x64-gnu": "16.2.11", + "@next/swc-linux-x64-musl": "16.2.11", + "@next/swc-win32-arm64-msvc": "16.2.11", + "@next/swc-win32-x64-msvc": "16.2.11", + "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", diff --git a/package.json b/package.json index cc66263..5d129d1 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "bcryptjs": "^3.0.3", "devtools-detector": "^2.0.25", "mysql2": "^3.23.1", - "next": "16.1.6", + "next": "^16.2.11", "next-auth": "^5.0.0-beta.32", "nextjs-toploader": "^3.9.17", "nodemailer": "^8.0.11", From 28308aee5b6976f05b25db0fa08e6892cc7bf768 Mon Sep 17 00:00:00 2001 From: Melvin Jones Repol Date: Sun, 23 Aug 2026 19:17:02 +0800 Subject: [PATCH 05/10] feat: bug fixes and improvements - fix issues with kanban boards - added email templates - initiate email verify upon registration - send welcome email after email verifying - drop kanban project color - added site grid-bg to auth pages --- app/(public)/(auth)/reset-password/page.tsx | 2 +- app/api/auth/register/route.ts | 3 + app/api/auth/verify-email/route.ts | 74 ++- app/api/kanban/issues/[id]/route.ts | 14 +- app/api/kanban/issues/route.ts | 25 +- app/api/kanban/projects/route.ts | 4 +- app/components/auth/ForgotPassword.tsx | 2 +- app/components/auth/Login.tsx | 2 +- app/components/auth/Logout.tsx | 2 +- app/components/auth/Signup.tsx | 2 +- app/components/auth/VerifyEmail.tsx | 4 +- app/components/auth/VerifyWakatime.tsx | 30 +- app/components/auth/form/LoginForm.tsx | 4 +- app/components/auth/form/SignupForm.tsx | 3 +- app/d/kanban/page.tsx | 470 ++++++++---------- app/lib/auth/verify-email.ts | 68 +++ app/lib/kanban.ts | 75 ++- app/lib/smtp/nodemailer.ts | 2 + app/lib/smtp/template.ts | 31 ++ .../migration.sql | 1 - prisma/schema.prisma | 1 - 21 files changed, 423 insertions(+), 396 deletions(-) create mode 100644 app/lib/auth/verify-email.ts create mode 100644 app/lib/smtp/template.ts rename prisma/migrations/{20260822201501_init => 20260823103009_init}/migration.sql (99%) diff --git a/app/(public)/(auth)/reset-password/page.tsx b/app/(public)/(auth)/reset-password/page.tsx index fc4b7fb..cf5da8e 100644 --- a/app/(public)/(auth)/reset-password/page.tsx +++ b/app/(public)/(auth)/reset-password/page.tsx @@ -47,7 +47,7 @@ export const metadata: Metadata = { export default async function ResetPassword() { return ( -
+
{/* Left Side - Visual / Branding */}
{/* Background elements */} diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts index 247da02..b54585f 100644 --- a/app/api/auth/register/route.ts +++ b/app/api/auth/register/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from "next/server"; import bcrypt from "bcryptjs"; import { prisma } from "@/app/lib/prisma"; import { recaptcha } from "@/app/lib/recaptcha"; +import verifyEmail from "@/app/lib/auth/verify-email"; export async function POST(req: Request) { const { email, password, token } = await req.json(); @@ -48,6 +49,8 @@ export async function POST(req: Request) { name: email.split("@")[0], }, }); + + verifyEmail(email); return NextResponse.json({ success: true }, { status: 201 }); } diff --git a/app/api/auth/verify-email/route.ts b/app/api/auth/verify-email/route.ts index 1116d82..10f4fe9 100644 --- a/app/api/auth/verify-email/route.ts +++ b/app/api/auth/verify-email/route.ts @@ -1,10 +1,9 @@ import { NextResponse } from "next/server"; import { prisma } from "@/app/lib/prisma"; -import crypto from "crypto"; -import { transporter } from "@/app/lib/smtp/nodemailer"; import { recaptcha } from "@/app/lib/recaptcha"; - -const NODE_MAILER_USER = process.env.NODE_MAILER_USER || ""; +import verifyEmail from "@/app/lib/auth/verify-email"; +import { NODE_MAILER_USER, transporter } from "@/app/lib/smtp/nodemailer"; +import emailTemplate from "@/app/lib/smtp/template"; export async function POST(req: Request) { const { email, token } = await req.json(); @@ -24,46 +23,7 @@ export async function POST(req: Request) { if (!(await recaptcha(token, "email_verify"))) throw new Error("reCAPTCHA verification failed. Please try again."); - const user = await prisma.user.findUnique({ where: { email } }); - - if (!user) { - return NextResponse.json({ success: true }); - } - - if (user.email_verified) { - return NextResponse.json({ success: true }); - } - - // Delete any existing verification token for this email before creating a new one - await prisma.verificationToken.deleteMany({ - where: { identifier: email }, - }); - - const verificationToken = crypto.randomBytes(32).toString("hex"); - const expires = new Date(Date.now() + 24 * 60 * 60 * 1000); - - await prisma.verificationToken.create({ - data: { identifier: email, token: verificationToken, expires }, - }); - - const verifyUrl = `${process.env.NEXTAUTH_URL}/api/auth/verify-email?token=${verificationToken}`; - - console.info(`Email verification link for ${email}: ${verifyUrl}`); - - transporter.sendMail({ - from: `Do Not Reply <${NODE_MAILER_USER}>`, - to: email, - subject: "Verify your email", - html: ` -

Hi ${user.name},

-

Please click the link below to verify your email address:

-

Verify Email

-

Regards,

-

DevPulse

- - This email was sent from DevPulse. If you did not request this, please ignore this email. - `, - }); + await verifyEmail(email); return NextResponse.json({ success: true }); } @@ -91,12 +51,36 @@ export async function GET(req: Request) { ); } - await prisma.user.update({ + const user = await prisma.user.update({ where: { email: record.identifier }, data: { email_verified: new Date() }, }); await prisma.verificationToken.delete({ where: { token } }); + transporter.sendMail({ + from: `Do Not Reply <${NODE_MAILER_USER}>`, + to: user.email, + subject: "Welcome to Devpulse", + html: emailTemplate({ + title: "Welcome to Devpulse", + bodyHtml: ` +

Hi ${user.name},

+

I'm Melvin Jones Repol, founder of Hall of Codes, the team behind Devpulse. Thank you for joining us, we're excited to have you on board.

+

Devpulse is built to help you measure and understand your coding pulse, and we're just getting started. Your feedback will play a big part in shaping where we go next.

+
+

+ Have a suggestion or ran into an issue? Please don't hesitate to reach out at + hallofcodes.org. + We'd love to hear from you. +

+
+

Welcome aboard, and happy coding!

+

Melvin Jones Repol

+

Founder, Hall of Codes

+ `, + }), + }); + return NextResponse.redirect(new URL("/login?verified=1", req.url)); } diff --git a/app/api/kanban/issues/[id]/route.ts b/app/api/kanban/issues/[id]/route.ts index df7476b..2eaabbb 100644 --- a/app/api/kanban/issues/[id]/route.ts +++ b/app/api/kanban/issues/[id]/route.ts @@ -27,7 +27,7 @@ export async function PATCH( if (column_id !== undefined) { const columnAccess = await getColumnAccess(session.user.id, column_id); - if (!columnAccess || columnAccess.projectId !== issueAccess.projectId) { + if (!columnAccess || columnAccess.project_id !== issueAccess.project_id) { return NextResponse.json( { error: "Cannot move issue to that column." }, { status: 400 }, @@ -36,7 +36,7 @@ export async function PATCH( } const data: Record = {}; - if (column_id !== undefined) data.columnId = column_id; + if (column_id !== undefined) data.column_id = column_id; if (position !== undefined) data.position = position; if (Object.keys(data).length === 0) { @@ -45,18 +45,22 @@ export async function PATCH( const issue = await prisma.issue.update({ where: { id }, - data: { ...data, updatedAt: new Date() }, + data: { ...data, updated_at: new Date() }, }); const payload = { type: "issue_updated", - data: { id: issue.id, column_id: issue.columnId, position: issue.position }, + data: { + id: issue.id, + column_id: issue.column_id, + position: issue.position, + }, }; emitter.emit("kanban", payload); return NextResponse.json({ id: issue.id, - column_id: issue.columnId, + column_id: issue.column_id, position: issue.position, }); } diff --git a/app/api/kanban/issues/route.ts b/app/api/kanban/issues/route.ts index 6616ca0..67357cb 100644 --- a/app/api/kanban/issues/route.ts +++ b/app/api/kanban/issues/route.ts @@ -23,33 +23,30 @@ export async function POST(req: Request) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const { columnId, title, tag, type, priority, issueKey, position } = + const { column_id, title, tag, type, priority, issue_key, position } = await req.json(); - if (!columnId || !title?.trim()) { + if (!column_id || !title?.trim()) { return NextResponse.json( { error: "columnId and title are required." }, { status: 400 }, ); } - const columnAccess = await getColumnAccess(session.user.id, columnId); + const columnAccess = await getColumnAccess(session.user.id, column_id); if (!columnAccess) { return NextResponse.json({ error: "Column not found." }, { status: 404 }); } const resolvedIssueKey = - typeof issueKey === "string" && issueKey.trim().length > 0 - ? issueKey.trim() - : await getNextIssueKey( - columnAccess.projectId, - columnAccess.projectName, - ); + typeof issue_key === "string" && issue_key.trim().length > 0 + ? issue_key.trim() + : await getNextIssueKey(columnAccess.project_id, columnAccess.project_name); const issue = await prisma.issue.create({ data: { - columnId, - issueKey: resolvedIssueKey, + column_id: column_id, + issue_key: resolvedIssueKey, title: title.trim(), tag: tag ?? "", type: TYPE_MAP[type] ?? "FEATURE", @@ -60,14 +57,14 @@ export async function POST(req: Request) { const payload = { id: issue.id, - column_id: issue.columnId, - issue_key: issue.issueKey, + column_id: issue.column_id, + issue_key: issue.issue_key, title: issue.title, tag: issue.tag ?? "", type: issue.type.toLowerCase(), priority: issue.priority.toLowerCase(), position: issue.position, - created_at: issue.createdAt.toISOString(), + created_at: issue.created_at.toISOString(), }; emitter.emit("kanban", { type: "issue_created", data: payload }); diff --git a/app/api/kanban/projects/route.ts b/app/api/kanban/projects/route.ts index 52f36be..a1ec0ff 100644 --- a/app/api/kanban/projects/route.ts +++ b/app/api/kanban/projects/route.ts @@ -27,7 +27,7 @@ export async function POST(req: Request) { const body = (await req.json()) as { name?: string; description?: string; - wakatimeProjectName?: string; + wakatime_project_name?: string; color?: string; }; @@ -57,7 +57,7 @@ export async function POST(req: Request) { const projectId = crypto.randomUUID(); const boardId = crypto.randomUUID(); const safeDescription = body.description?.trim() || null; - const safeWakaName = body.wakatimeProjectName?.trim() || null; + const safeWakaName = body.wakatime_project_name?.trim() || null; const safeColor = body.color?.trim() || "blue"; const now = new Date(); diff --git a/app/components/auth/ForgotPassword.tsx b/app/components/auth/ForgotPassword.tsx index e768a1f..24242ac 100644 --- a/app/components/auth/ForgotPassword.tsx +++ b/app/components/auth/ForgotPassword.tsx @@ -18,7 +18,7 @@ export default function ForgotPassword() { : undefined; return ( -
+
{/* Left Side - Visual / Branding */}
{/* Background elements */} diff --git a/app/components/auth/Login.tsx b/app/components/auth/Login.tsx index 33260bb..ee265b7 100644 --- a/app/components/auth/Login.tsx +++ b/app/components/auth/Login.tsx @@ -18,7 +18,7 @@ export default function Login() { : undefined; return ( -
+
{/* Left Side - Visual / Branding */}
{/* Background elements */} diff --git a/app/components/auth/Logout.tsx b/app/components/auth/Logout.tsx index 6c38aa3..cb3d48c 100644 --- a/app/components/auth/Logout.tsx +++ b/app/components/auth/Logout.tsx @@ -22,7 +22,7 @@ export default function Logout() { }, [handleLogout]); return ( -
+
); diff --git a/app/components/auth/Signup.tsx b/app/components/auth/Signup.tsx index 6d003ab..a2d1351 100644 --- a/app/components/auth/Signup.tsx +++ b/app/components/auth/Signup.tsx @@ -18,7 +18,7 @@ export default function Signup() { : undefined; return ( -
+
{/* Left Side - Visual / Branding */}
{/* Background elements */} diff --git a/app/components/auth/VerifyEmail.tsx b/app/components/auth/VerifyEmail.tsx index f5628d5..7c79f34 100644 --- a/app/components/auth/VerifyEmail.tsx +++ b/app/components/auth/VerifyEmail.tsx @@ -81,7 +81,7 @@ export default function VerifyEmail({ }; return ( -
+
{/* Left Side - Visual / Branding */}
@@ -250,7 +250,7 @@ export default function VerifyEmail({ disabled={loading} className="w-full py-3 rounded-xl font-semibold btn-primary disabled:opacity-50 disabled:cursor-not-allowed" > - esend verification email + Resend verification email )} diff --git a/app/components/auth/VerifyWakatime.tsx b/app/components/auth/VerifyWakatime.tsx index 7dd52d2..674af20 100644 --- a/app/components/auth/VerifyWakatime.tsx +++ b/app/components/auth/VerifyWakatime.tsx @@ -57,25 +57,25 @@ export default function VerifyWakatime() { toast.promise(verifyWakatimePromise, { pending: "Verifying...", - success: "Verification successful!", - error: "Failed to verify. Please try again.", + success: { + render() { + router.push("/d"); + + return "Verification successful!"; + }, + }, + error: { + render({ data }) { + setLoading(false); + const err = data as Error; + return err?.message || "Failed to verify. Please try again."; + }, + }, }); - - verifyWakatimePromise - .then(() => { - router.push("/d"); - }) - .catch(() => { - // already surfaced via toast.promise - // avoid unhandled rejection - }) - .finally(() => { - setLoading(false); - }); }; return ( -
+
{/* Left Side - Visual / Branding */}
diff --git a/app/components/auth/form/LoginForm.tsx b/app/components/auth/form/LoginForm.tsx index abd466d..496ce44 100644 --- a/app/components/auth/form/LoginForm.tsx +++ b/app/components/auth/form/LoginForm.tsx @@ -44,7 +44,7 @@ export default function LoginForm() { const handleLogin = async (e: React.SyntheticEvent) => { e.preventDefault(); - + if (!grecaptchaLoaded || !window.grecaptcha?.enterprise) { toast.error( "Recaptcha Enterprise is not loaded. Please try again later.", @@ -97,7 +97,7 @@ export default function LoginForm() { <> {justVerified && (
- Email verified successfully. You can now log in. + Thank you for verifying your email!
)}
diff --git a/app/components/auth/form/SignupForm.tsx b/app/components/auth/form/SignupForm.tsx index ee1e48e..ee55275 100644 --- a/app/components/auth/form/SignupForm.tsx +++ b/app/components/auth/form/SignupForm.tsx @@ -90,7 +90,8 @@ export default function SignupForm() { render() { setLoading(false); router.push(`/verify-email?email=${encodeURIComponent(email)}`); - return "Account created! Please verify your email."; + + return "Please check your email to verify your account."; }, }, error: { diff --git a/app/d/kanban/page.tsx b/app/d/kanban/page.tsx index 75cdf1f..dc8b6f0 100644 --- a/app/d/kanban/page.tsx +++ b/app/d/kanban/page.tsx @@ -13,6 +13,8 @@ import { useSensors, } from "@dnd-kit/core"; import { toast } from "react-toastify"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; interface KanbanIssue { id: string; @@ -45,7 +47,6 @@ interface KanbanProject { name: string; description: string; wakatime_project_name: string; - color: string; created_at: string; board_count: number; column_count: number; @@ -78,22 +79,6 @@ function isKanbanProject(value: unknown): value is KanbanProject { ); } -const PROJECT_COLORS = [ - { value: "blue", label: "blue" }, - { value: "cyan", label: "Cyan" }, - { value: "emerald", label: "Emerald" }, - { value: "amber", label: "Amber" }, - { value: "rose", label: "Rose" }, -]; - -const COLOR_STYLES: Record = { - blue: "from-blue-500/25 to-violet-500/10 border-blue-500/20", - cyan: "from-cyan-500/25 to-sky-500/10 border-cyan-500/20", - emerald: "from-emerald-500/25 to-teal-500/10 border-emerald-500/20", - amber: "from-amber-500/25 to-orange-500/10 border-amber-500/20", - rose: "from-rose-500/25 to-pink-500/10 border-rose-500/20", -}; - function formatHours(seconds: number) { return `${(seconds / 3600).toFixed(seconds >= 36000 ? 0 : 1)}h`; } @@ -164,8 +149,7 @@ export default function Kanban() { const [projectForm, setProjectForm] = useState({ name: "", description: "", - wakatimeproject_name: "", - color: "blue", + wakatime_project_name: "", }); const load = useCallback(async () => { @@ -291,9 +275,10 @@ export default function Kanban() { return issues.filter((issue) => doneColIds.has(issue.column_id)).length; }, [issues, availableColumns]); - const liveCompletionRate = liveIssueCount > 0 - ? Math.round((liveCompletedCount / liveIssueCount) * 100) - : 0; + const liveCompletionRate = + liveIssueCount > 0 + ? Math.round((liveCompletedCount / liveIssueCount) * 100) + : 0; const recentIssues = useMemo(() => { return issues @@ -312,11 +297,12 @@ export default function Kanban() { if (!over) return; const issueId = String(active.id); - const destinationColumnId = String(over.data.current?.columnId ?? ""); + const destinationColumnId = String(over.data.current?.column_id ?? ""); if (!destinationColumnId) return; const destinationIssues = issues.filter( - (issue) => issue.column_id === destinationColumnId && issue.id !== issueId, + (issue) => + issue.column_id === destinationColumnId && issue.id !== issueId, ); const position = destinationIssues.length; @@ -354,7 +340,7 @@ export default function Kanban() { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - columnId: selectedColumn, + column_id: selectedColumn, title: issueForm.title, tag: issueForm.tag, type: issueForm.type, @@ -414,8 +400,7 @@ export default function Kanban() { setProjectForm({ name: "", description: "", - wakatimeproject_name: "", - color: "blue", + wakatime_project_name: "", }); setProjectModalOpen(false); toast.success("Kanban project created."); @@ -430,99 +415,80 @@ export default function Kanban() { } return ( -
- {/* Sticky header — title + actions only */} -
-
-
-
-

- Project Kanban -

-

Boards

-
- -
-
+
+ {/* Metrics row */} +
+ + + +
- {/* Main content */} -
- {/* Metrics row */} -
- - - - -
- - {/* Workspace + Boards */} -
- {/* Left sidebar — shown below boards on mobile, beside on xl */} -
-
-
-
-

- Project Workspace -

-

- Switch context or create a new project. -

-
+ {/* Workspace + Boards */} +
+ {/* Left sidebar — shown below boards on mobile, beside on xl */} +
+
+
+
+

+ Project Workspace +

+

+ Switch context or create a new project. +

+ +
-
- - -
+
+ + +
+
{currentProject ? ( -
+

@@ -532,9 +498,6 @@ export default function Kanban() { {currentProject.description || "No project brief yet."}

- - {currentProject.color} -
@@ -565,127 +528,132 @@ export default function Kanban() { /> )}
- -
- - -
- {recentIssues.length > 0 ? ( - recentIssues.map((issue) => ( -
-
- {issue.issue_key} - - {issue.priority} - -
-

- {issue.title} -

-
- {issue.tag ? ( - - {issue.tag} - - ) : null} - - {issue.type} - - {formatDate(issue.created_at)} -
-
- )) - ) : ( - - )} -
-
- {/* Boards panel — shown first on mobile */} -
-
+
+
+ + {showRecentIssues ? "▲" : "▼"} + + - {loading ? ( -
- Loading Kanban workspace... -
- ) : groupedBoards.length === 0 ? ( -
+
+ {recentIssues.length > 0 ? ( + recentIssues.map((issue) => ( +
+
+ {issue.issue_key} + + {issue.priority} + +
+

+ {issue.title} +

+
+ {issue.tag ? ( + + {issue.tag} + + ) : null} + + {issue.type} + + {formatDate(issue.created_at)} +
+
+ )) + ) : ( + )} +
+
+
+ + {/* Boards panel — shown first on mobile */} +
+
+
+

Boards

+

+ {currentProject + ? "Delivery lanes for the active project." + : "Create a project first to open a board."} +

+
+ {currentProject?.wakatime_project_name ? ( +
+ Bound to {currentProject.wakatime_project_name}
- ) : ( - -
- {groupedBoards.map((board) => ( -
-
-

- {board.title} -

-

- {board.description || "Execution board"} -

-
- - {/* Horizontal scroll on mobile, grid on md+ */} -
- {board.columns.map((column) => ( -
- { - setSelectedColumn(column.id); - setIssueModalOpen(true); - }} - /> -
- ))} -
-
- ))} -
-
- )} + ) : null}
+ + {loading ? ( +
+ Loading Kanban workspace... +
+ ) : groupedBoards.length === 0 ? ( +
+ +
+ ) : ( + +
+ {groupedBoards.map((board) => ( +
+
+

+ {board.title} +

+

+ {board.description || "Execution board"} +

+
+ + {/* Horizontal scroll on mobile, grid on md+ */} +
+ {board.columns.map((column) => ( +
+ { + setSelectedColumn(column.id); + setIssueModalOpen(true); + }} + /> +
+ ))} +
+
+ ))} +
+
+ )}
@@ -721,11 +689,11 @@ export default function Kanban() { /> - -
-

- Accent -

-
- {PROJECT_COLORS.map((color) => ( - - ))} -
-
@@ -894,7 +835,9 @@ function OverviewCard({ }) { return (
-

{label}

+

+ {label} +

{value}

{sub}

@@ -951,13 +894,13 @@ function Column({ }) { const { setNodeRef } = useDroppable({ id: column.id, - data: { columnId: column.id }, + data: { column_id: column.id }, }); return (
@@ -992,10 +935,11 @@ function IssueCard({ item: KanbanIssue; columnId: string; }) { - const { setNodeRef, listeners, attributes, transform, isDragging } = useDraggable({ - id: item.id, - data: { columnId }, - }); + const { setNodeRef, listeners, attributes, transform, isDragging } = + useDraggable({ + id: item.id, + data: { column_id: columnId }, + }); return (
`, + to: email, + subject: "Verify your email", + html: emailTemplate({ + title: "Verify your email", + bodyHtml: ` +

Hi ${user.name},

+

Thanks for signing up! Please confirm your email address by clicking the button below.

+ +

+ Or copy and paste this link into your browser:
+ ${verifyUrl} +

+

+ This link will expire in 6 hours. If you didn't create a Devpulse account, you can safely ignore this email. +

+ `, + }), + }); + } catch (error) { + console.error(error); + } +} diff --git a/app/lib/kanban.ts b/app/lib/kanban.ts index 3cc7256..f82631a 100644 --- a/app/lib/kanban.ts +++ b/app/lib/kanban.ts @@ -12,7 +12,6 @@ interface KanbanProjectRow { name: string; description: string | null; wakatime_project_name: string | null; - color: string | null; created_at: Date; } @@ -62,7 +61,7 @@ function normalizeWakaTimeProjects(value: unknown): WakaTimeProject[] { } export async function getKanbanProjectAccess( - user_id: string, + userId: string, projectId: string, ): Promise { const rows = await prisma.$queryRaw>` @@ -77,27 +76,24 @@ export async function getKanbanProjectAccess( } export async function getColumnAccess( - user_id: string, + userId: string, columnId: string, -): Promise< - | { - columnId: string; - projectId: string; - project_name: string; - } - | null -> { +): Promise<{ + column_id: string; + project_id: string; + project_name: string; +} | null> { const rows = await prisma.$queryRaw< Array<{ - columnId: string; - projectId: string; + column_id: string; + project_id: string; project_name: string; }> >` SELECT - c.id AS columnId, - p.id AS projectId, - p.name AS projectName + c.id AS column_id, + p.id AS project_id, + p.name AS project_name FROM columns c INNER JOIN boards b ON b.id = c.board_id INNER JOIN projects p ON p.id = b.project_id @@ -110,24 +106,21 @@ export async function getColumnAccess( } export async function getIssueAccess( - user_id: string, + userId: string, issueId: string, -): Promise< - | { - issueId: string; - projectId: string; - } - | null -> { +): Promise<{ + issue_id: string; + project_id: string; +} | null> { const rows = await prisma.$queryRaw< Array<{ - issueId: string; - projectId: string; + issue_id: string; + project_id: string; }> >` SELECT - i.id AS issueId, - p.id AS projectId + i.id AS issue_id, + p.id AS project_id FROM issues i INNER JOIN columns c ON c.id = i.column_id INNER JOIN boards b ON b.id = c.board_id @@ -140,7 +133,7 @@ export async function getIssueAccess( return rows[0] ?? null; } -export async function getNextIssueKey(projectId: string, project_name: string) { +export async function getNextIssueKey(projectId: string, projectName: string) { const rows = await prisma.$queryRaw>` SELECT COUNT(*) AS issueCount FROM issues i @@ -153,18 +146,19 @@ export async function getNextIssueKey(projectId: string, project_name: string) { const issueCount = typeof rawCount === "bigint" ? Number(rawCount) : Number(rawCount); - const prefix = projectName - .replace(/[^a-zA-Z0-9 ]/g, " ") - .split(/\s+/) - .filter(Boolean) - .slice(0, 3) - .map((part) => part[0]?.toUpperCase() ?? "") - .join("") || "DP"; + const prefix = + projectName + .replace(/[^a-zA-Z0-9 ]/g, " ") + .split(/\s+/) + .filter(Boolean) + .slice(0, 3) + .map((part) => part[0]?.toUpperCase() ?? "") + .join("") || "DP"; return `${prefix}-${String(issueCount + 1).padStart(3, "0")}`; } -export async function getKanbanData(user_id: string) { +export async function getKanbanData(userId: string) { const [projectRows, boardRows, columnRows, issueRows, userProjects] = await Promise.all([ prisma.$queryRaw` @@ -221,7 +215,7 @@ export async function getKanbanData(user_id: string) { ORDER BY i.position ASC, i.created_at ASC `, prisma.userProjects.findUnique({ - where: { userId }, + where: { user_id: userId }, select: { projects: true }, }), ]); @@ -229,7 +223,9 @@ export async function getKanbanData(user_id: string) { const projects = projectRows.map((project) => { const boards = boardRows.filter((board) => board.project_id === project.id); const boardIds = new Set(boards.map((board) => board.id)); - const columns = columnRows.filter((column) => boardIds.has(column.board_id)); + const columns = columnRows.filter((column) => + boardIds.has(column.board_id), + ); const columnIds = new Set(columns.map((column) => column.id)); const issues = issueRows.filter((issue) => columnIds.has(issue.column_id)); const doneColumnIds = new Set( @@ -246,7 +242,6 @@ export async function getKanbanData(user_id: string) { name: project.name, description: project.description ?? "", wakatime_project_name: project.wakatime_project_name ?? "", - color: project.color ?? "blue", created_at: project.created_at.toISOString(), board_count: boards.length, column_count: columns.length, diff --git a/app/lib/smtp/nodemailer.ts b/app/lib/smtp/nodemailer.ts index 5937564..f5b60bf 100644 --- a/app/lib/smtp/nodemailer.ts +++ b/app/lib/smtp/nodemailer.ts @@ -1,5 +1,7 @@ import nodemailer from "nodemailer"; +export const NODE_MAILER_USER = process.env.NODE_MAILER_USER || ""; + const transporter = nodemailer.createTransport({ host: process.env.NODE_MAILER_HOST || "smtp.gmail.com", port: process.env.NODE_MAILER_PORT diff --git a/app/lib/smtp/template.ts b/app/lib/smtp/template.ts new file mode 100644 index 0000000..6e28e58 --- /dev/null +++ b/app/lib/smtp/template.ts @@ -0,0 +1,31 @@ +/** + * Generates an HTML email template with the given title and body. + * + * @param title The title of the email. + * @param bodyHtml The HTML content of the email body. + * @returns The HTML email template as a string. + */ +export default function emailTemplate({ + title, + bodyHtml, +}: { + title?: string; + bodyHtml: string; +}) { + return ` +
+
+

Devpulse

+

Measure your coding pulse.

+
+
+ ${title ? `

${title}

` : ""} + ${bodyHtml} +
+
+

This email was sent from Devpulse.

+

© 2026 Devpulse. All rights reserved.

+
+
+ `; +} diff --git a/prisma/migrations/20260822201501_init/migration.sql b/prisma/migrations/20260823103009_init/migration.sql similarity index 99% rename from prisma/migrations/20260822201501_init/migration.sql rename to prisma/migrations/20260823103009_init/migration.sql index 5716f97..acfdfd5 100644 --- a/prisma/migrations/20260822201501_init/migration.sql +++ b/prisma/migrations/20260823103009_init/migration.sql @@ -203,7 +203,6 @@ CREATE TABLE `projects` ( `name` VARCHAR(191) NOT NULL, `description` VARCHAR(191) NULL, `wakatime_project_name` VARCHAR(191) NULL, - `color` VARCHAR(191) NULL DEFAULT 'blue', `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), INDEX `projects_user_id_idx`(`user_id`), diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 39e1c17..5276fa7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -239,7 +239,6 @@ model Project { name String description String? wakatime_project_name String? - color String? @default("blue") created_at DateTime @default(now()) user User? @relation(fields: [user_id], references: [id], onDelete: Cascade) From 5f48abf1f0be78452ffcb8b9c9dec5351c5733aa Mon Sep 17 00:00:00 2001 From: Melvin Jones Repol Date: Mon, 24 Aug 2026 02:07:47 +0800 Subject: [PATCH 06/10] feat(leaderboards): simplify leaderboards & flex UI and bug fixes --- app/(public)/join/[code]/page.tsx | 2 +- app/(public)/join/page.tsx | 4 +- app/(public)/leaderboard/[slug]/page.tsx | 18 +-- app/api/flex/route.ts | 4 +- app/api/kanban/projects/route.ts | 4 - app/api/leaderboards/[id]/join-code/route.ts | 14 +- app/api/leaderboards/[id]/route.ts | 4 +- app/api/leaderboards/join/route.ts | 6 +- app/api/leaderboards/route.ts | 8 +- app/components/BoardList.tsx | 23 +-- app/components/Flex.tsx | 50 +++---- app/components/JoinButton.tsx | 2 +- app/components/dashboard/Leaderboards.tsx | 6 +- app/components/dashboard/LeaderbordList.tsx | 140 ++++++------------- app/d/kanban/page.tsx | 3 +- app/d/leaderboards/page.tsx | 6 +- app/lib/kanban.ts | 1 - 17 files changed, 102 insertions(+), 193 deletions(-) diff --git a/app/(public)/join/[code]/page.tsx b/app/(public)/join/[code]/page.tsx index 87feb63..7d8abfa 100644 --- a/app/(public)/join/[code]/page.tsx +++ b/app/(public)/join/[code]/page.tsx @@ -10,7 +10,7 @@ export async function generateMetadata({ params }: Props): Promise { const { code } = await params; const leaderboard = await prisma.leaderboard.findUnique({ - where: { joinCode: code }, + where: { join_code: code }, select: { name: true, description: true }, }); diff --git a/app/(public)/join/page.tsx b/app/(public)/join/page.tsx index a71bcab..d3fd77e 100644 --- a/app/(public)/join/page.tsx +++ b/app/(public)/join/page.tsx @@ -19,13 +19,13 @@ type Props = { async function getLeaderboard(code: string) { return prisma.leaderboard.findUnique({ - where: { joinCode: code }, + where: { join_code: code }, select: { id: true, name: true, description: true, slug: true, - ownerId: true, + owner_id: true, created_at: true, }, }); diff --git a/app/(public)/leaderboard/[slug]/page.tsx b/app/(public)/leaderboard/[slug]/page.tsx index 0ee1e89..b9f0d38 100644 --- a/app/(public)/leaderboard/[slug]/page.tsx +++ b/app/(public)/leaderboard/[slug]/page.tsx @@ -40,11 +40,11 @@ export default async function LeaderboardPage(props: { id: true, email: true, role: true, - userStats: { + user_stats: { select: { - totalSeconds: true, + total_seconds: true, languages: true, - operatingSystems: true, + operating_systems: true, editors: true, }, }, @@ -56,14 +56,14 @@ export default async function LeaderboardPage(props: { members = rows .filter((r) => r.user.email) .map((r) => ({ - user_id: r.userId, + user_id: r.user.id, role: r.role, email: r.user.email!, - total_seconds: Number(r.user.userStats?.totalSeconds ?? 0), - languages: (r.user.userStats?.languages as { name: string }[]) ?? [], + total_seconds: Number(r.user.user_stats?.total_seconds ?? 0), + languages: (r.user.user_stats?.languages as { name: string }[]) ?? [], operating_systems: - (r.user.userStats?.operatingSystems as { name: string }[]) ?? [], - editors: (r.user.userStats?.editors as { name: string }[]) ?? [], + (r.user.user_stats?.operating_systems as { name: string }[]) ?? [], + editors: (r.user.user_stats?.editors as { name: string }[]) ?? [], })); } catch { return InternalServerError(); @@ -112,7 +112,7 @@ export default async function LeaderboardPage(props: {
diff --git a/app/api/flex/route.ts b/app/api/flex/route.ts index 609b87e..5ae38a4 100644 --- a/app/api/flex/route.ts +++ b/app/api/flex/route.ts @@ -44,14 +44,14 @@ export async function POST(req: Request) { const flex = await prisma.userFlex.create({ data: { user_id: session.user.id, - userEmail: session.user.email, + user_email: session.user.email, project_name: project_name.trim(), project_description: project_description ?? "", project_url: project_url ?? "", project_time: project_time ?? "", is_open_source: is_open_source ?? false, open_source_url: is_open_source ? (open_source_url ?? "") : "", - expiresAt, + expires_at: expiresAt, }, }); diff --git a/app/api/kanban/projects/route.ts b/app/api/kanban/projects/route.ts index a1ec0ff..3c14a74 100644 --- a/app/api/kanban/projects/route.ts +++ b/app/api/kanban/projects/route.ts @@ -28,7 +28,6 @@ export async function POST(req: Request) { name?: string; description?: string; wakatime_project_name?: string; - color?: string; }; const name = body.name?.trim(); @@ -58,7 +57,6 @@ export async function POST(req: Request) { const boardId = crypto.randomUUID(); const safeDescription = body.description?.trim() || null; const safeWakaName = body.wakatime_project_name?.trim() || null; - const safeColor = body.color?.trim() || "blue"; const now = new Date(); await prisma.$transaction(async (tx) => { @@ -69,7 +67,6 @@ export async function POST(req: Request) { name, description, wakatime_project_name, - color, created_at ) VALUES ( ${projectId}, @@ -77,7 +74,6 @@ export async function POST(req: Request) { ${name}, ${safeDescription}, ${safeWakaName}, - ${safeColor}, ${now} ) `; diff --git a/app/api/leaderboards/[id]/join-code/route.ts b/app/api/leaderboards/[id]/join-code/route.ts index 696cf11..b7fda43 100644 --- a/app/api/leaderboards/[id]/join-code/route.ts +++ b/app/api/leaderboards/[id]/join-code/route.ts @@ -15,14 +15,14 @@ export async function GET( const leaderboard = await prisma.leaderboard.findUnique({ where: { id }, - select: { joinCode: true, ownerId: true }, + select: { join_code: true, owner_id: true }, }); - if (!leaderboard || leaderboard.ownerId !== session.user.id) { + if (!leaderboard || leaderboard.owner_id !== session.user.id) { return NextResponse.json({ error: "Forbidden." }, { status: 403 }); } - return NextResponse.json({ joinCode: leaderboard.joinCode }); + return NextResponse.json({ join_code: leaderboard.join_code }); } export async function PATCH( @@ -38,10 +38,10 @@ export async function PATCH( const leaderboard = await prisma.leaderboard.findUnique({ where: { id }, - select: { ownerId: true }, + select: { owner_id: true }, }); - if (!leaderboard || leaderboard.ownerId !== session.user.id) { + if (!leaderboard || leaderboard.owner_id !== session.user.id) { return NextResponse.json({ error: "Forbidden." }, { status: 403 }); } @@ -49,8 +49,8 @@ export async function PATCH( await prisma.leaderboard.update({ where: { id }, - data: { joinCode }, + data: { join_code: joinCode }, }); - return NextResponse.json({ success: true, joinCode }); + return NextResponse.json({ success: true, join_code: joinCode }); } diff --git a/app/api/leaderboards/[id]/route.ts b/app/api/leaderboards/[id]/route.ts index 1d2134f..2fc048b 100644 --- a/app/api/leaderboards/[id]/route.ts +++ b/app/api/leaderboards/[id]/route.ts @@ -15,14 +15,14 @@ export async function DELETE( const leaderboard = await prisma.leaderboard.findUnique({ where: { id }, - select: { ownerId: true }, + select: { owner_id: true }, }); if (!leaderboard) { return NextResponse.json({ error: "Not found." }, { status: 404 }); } - if (leaderboard.ownerId !== session.user.id) { + if (leaderboard.owner_id !== session.user.id) { return NextResponse.json({ error: "Forbidden." }, { status: 403 }); } diff --git a/app/api/leaderboards/join/route.ts b/app/api/leaderboards/join/route.ts index a70df43..6d1363e 100644 --- a/app/api/leaderboards/join/route.ts +++ b/app/api/leaderboards/join/route.ts @@ -8,8 +8,8 @@ export async function POST(req: Request) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const { joinCode } = await req.json(); - if (!joinCode) { + const { join_code } = await req.json(); + if (!join_code) { return NextResponse.json( { error: "Join code is required." }, { status: 400 }, @@ -17,7 +17,7 @@ export async function POST(req: Request) { } const leaderboard = await prisma.leaderboard.findUnique({ - where: { joinCode }, + where: { join_code }, select: { id: true, slug: true }, }); diff --git a/app/api/leaderboards/route.ts b/app/api/leaderboards/route.ts index d8ee22a..eb5998d 100644 --- a/app/api/leaderboards/route.ts +++ b/app/api/leaderboards/route.ts @@ -23,9 +23,9 @@ export async function POST(req: Request) { name: name.trim(), description: "", slug, - ownerId: session.user.id, - joinCode, - isPublic: true, + owner_id: session.user.id, + join_code: joinCode, + is_public: true, }, }); @@ -38,7 +38,7 @@ export async function POST(req: Request) { }); return NextResponse.json( - { joinCode: leaderboard.joinCode }, + { join_code: leaderboard.join_code }, { status: 201 }, ); } catch { diff --git a/app/components/BoardList.tsx b/app/components/BoardList.tsx index 25aac6d..a37968a 100644 --- a/app/components/BoardList.tsx +++ b/app/components/BoardList.tsx @@ -8,8 +8,6 @@ import { faKey, faRotateRight, faTrashAlt, - faChevronRight, - faServer, faRightFromBracket, } from "@fortawesome/free-solid-svg-icons"; import { toast } from "react-toastify"; @@ -102,7 +100,7 @@ export default function BoardList({ async (res) => { const data = await res.json(); if (!res.ok) throw new Error(data.error); - return data.joinCode as string; + return data.join_code as string; }, ); @@ -124,17 +122,11 @@ export default function BoardList({ return ( <> -
+
-
- -

@@ -145,13 +137,6 @@ export default function BoardList({ /{board.slug}

- -
- -
{user.id === board.owner_id && ( @@ -200,7 +185,6 @@ export default function BoardList({ {showCodeModal && (
-

Share Server @@ -255,7 +239,7 @@ export default function BoardList({ createPortal(
-
+

Leave leaderboard?

@@ -292,7 +276,6 @@ export default function BoardList({ {showDeleteModal && (
-

Delete Network diff --git a/app/components/Flex.tsx b/app/components/Flex.tsx index e7acca1..fe38f78 100644 --- a/app/components/Flex.tsx +++ b/app/components/Flex.tsx @@ -39,13 +39,13 @@ interface FlexRow { function toEditableFlex(row: FlexRow): Projects { return { - name: row.projectName ?? "", - text: row.projectTime ?? "", - project_description: row.projectDescription ?? "", - project_url: row.projectUrl ?? "", - project_time: row.projectTime ?? "", - is_open_source: row.isOpenSource ?? false, - open_source_url: row.openSourceUrl ?? "", + name: row.project_name ?? "", + text: row.project_time ?? "", + project_description: row.project_description ?? "", + project_url: row.project_url ?? "", + project_time: row.project_time ?? "", + is_open_source: row.is_open_source ?? false, + open_source_url: row.open_source_url ?? "", }; } @@ -187,7 +187,7 @@ export default function Flex() { const data: { projects: Projects[] } = await res.json(); const projects: Projects[] = data.projects ?? []; const newProjects = projects.filter( - (p) => !userFlexes.some((f) => f.projectName === p.name), + (p) => !userFlexes.some((f) => f.project_name === p.name), ); setFlexes(newProjects); } @@ -197,19 +197,13 @@ export default function Flex() { return (
-
+
-

+

Flex

-

- - - Share your flexes with the community - +

+ Share your projects with the community

@@ -327,7 +321,7 @@ export default function Flex() { {userFlexes.map((f) => (
-

{f.projectName}

+

{f.project_name}

-

{f.projectDescription}

+

{f.project_description}

- {f.projectUrl} + {f.project_url} - {f.isOpenSource && ( + {f.is_open_source && ( - {f.openSourceUrl} + {f.open_source_url} )} - Expires in {expireAt(f.expiresAt || "")} • Posted{" "} - {timeAgo(f.createdAt)} + Expires in {expireAt(f.expires_at || "")} • Posted{" "} + {timeAgo(f.created_at)}
))} diff --git a/app/components/JoinButton.tsx b/app/components/JoinButton.tsx index 8b0b801..adb7a5c 100644 --- a/app/components/JoinButton.tsx +++ b/app/components/JoinButton.tsx @@ -68,7 +68,7 @@ export default function JoinButton({ const joinPromise = fetch("/api/leaderboards/join", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ joinCode: code }), + body: JSON.stringify({ join_code: code }), }).then(async (res) => { const data = await res.json(); if (!res.ok) throw new Error(data.error); diff --git a/app/components/dashboard/Leaderboards.tsx b/app/components/dashboard/Leaderboards.tsx index 77296ea..7cb8e72 100644 --- a/app/components/dashboard/Leaderboards.tsx +++ b/app/components/dashboard/Leaderboards.tsx @@ -49,7 +49,7 @@ export default function Leaderboards() { const body = await res.json(); throw new Error(body.error || "Failed to create. Please try again."); } - return res.json() as Promise<{ joinCode: string }>; + return res.json() as Promise<{ join_code: string }>; }); toast.promise(createPromise, { @@ -65,7 +65,7 @@ export default function Leaderboards() { }); createPromise.then((data) => { - setCreatedCode(data.joinCode); + setCreatedCode(data.join_code); setLeaderboardName(""); setActiveModal("share"); }); @@ -80,7 +80,7 @@ export default function Leaderboards() { const joinPromise = fetch("/api/leaderboards/join", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ joinCode: joinCode.trim() }), + body: JSON.stringify({ join_code: joinCode.trim() }), }).then(async (res) => { if (!res.ok) { const body = await res.json(); diff --git a/app/components/dashboard/LeaderbordList.tsx b/app/components/dashboard/LeaderbordList.tsx index 89b7407..99f038a 100644 --- a/app/components/dashboard/LeaderbordList.tsx +++ b/app/components/dashboard/LeaderbordList.tsx @@ -8,7 +8,7 @@ export interface Leaderboard { id: string; name: string; slug: string; - ownerId: string; + owner_id: string; } export default async function LeaderboardsList() { @@ -17,123 +17,63 @@ export default async function LeaderboardsList() { const [owned, memberships] = await Promise.all([ prisma.leaderboard.findMany({ - where: { ownerId: user.id }, - select: { id: true, name: true, slug: true, ownerId: true }, + where: { owner_id: user.id }, + select: { id: true, name: true, slug: true, owner_id: true }, }), prisma.leaderboardMember.findMany({ where: { user_id: user.id, role: "member" }, include: { leaderboard: { - select: { id: true, name: true, slug: true, ownerId: true }, + select: { id: true, name: true, slug: true, owner_id: true }, }, }, }), ]); const joinedBoards = memberships.map((m) => m.leaderboard); - const ownedCount = owned.length; - const joinedCount = joinedBoards.length; + + const allBoards = [ + ...owned.map((board) => ({ board, isOwner: true })), + ...joinedBoards.map((board) => ({ board, isOwner: false })), + ]; const userForBoard = { id: user.id, email: user.email ?? "" }; return ( -
-
- -
-
-

- - Your Networks -

- - {ownedCount + joinedCount} Active Sessions - -
-
- -
- {owned.length > 0 && ( -
-
- -

- Administered -

-
-
-
- {owned.map((board) => ( -
-
- -
- ))} -
-
- )} - - {joinedBoards.length > 0 && ( -
-
- -

- Joined Networks -

-
-
-
- {joinedBoards.map((board) => ( -
-
- + {allBoards.length > 0 ? ( +
+
+ {allBoards.map(({ board, isOwner }) => ( +
+ {isOwner && ( + -
- ))} -
+ )} + +
+ ))}
- )} - - {!ownedCount && !joinedCount && ( -
-
- -
-

- No Active Networks -

-

- Create a new network or join an existing server to start - competing. -

+
+ ) : ( +
+
+
- )} -
+

+ You haven't joined any boards yet. +

+

+ Create a new network or join an existing server to start competing. +

+
+ )}
); } diff --git a/app/d/kanban/page.tsx b/app/d/kanban/page.tsx index dc8b6f0..b2e2230 100644 --- a/app/d/kanban/page.tsx +++ b/app/d/kanban/page.tsx @@ -69,7 +69,6 @@ function isKanbanProject(value: unknown): value is KanbanProject { typeof project.name === "string" && typeof project.description === "string" && typeof project.wakatime_project_name === "string" && - typeof project.color === "string" && typeof project.created_at === "string" && typeof project.board_count === "number" && typeof project.column_count === "number" && @@ -495,7 +494,7 @@ export default function Kanban() { {currentProject.name}

- {currentProject.description || "No project brief yet."} + {currentProject.description}

diff --git a/app/d/leaderboards/page.tsx b/app/d/leaderboards/page.tsx index 0158ed4..6102666 100644 --- a/app/d/leaderboards/page.tsx +++ b/app/d/leaderboards/page.tsx @@ -13,7 +13,7 @@ export default async function LeaderboardsPage() { if (!user) return redirect("/login?from=/leaderboards"); return ( -
+

@@ -26,9 +26,7 @@ export default async function LeaderboardsPage() {

-
- -
+
); } diff --git a/app/lib/kanban.ts b/app/lib/kanban.ts index f82631a..636b1f7 100644 --- a/app/lib/kanban.ts +++ b/app/lib/kanban.ts @@ -167,7 +167,6 @@ export async function getKanbanData(userId: string) { p.name, p.description, p.wakatime_project_name, - p.color, p.created_at FROM projects p WHERE p.user_id = ${userId} OR p.user_id IS NULL From 2437fb46c27de98a45a98fe14b9da0ba859f635a Mon Sep 17 00:00:00 2001 From: Melvin Jones Repol Date: Mon, 24 Aug 2026 03:38:35 +0800 Subject: [PATCH 07/10] feat(account-settings): bug fixes and improvements --- app/api/auth/forgot-password/route.ts | 36 ++++- app/components/common/NavProfileDropdown.tsx | 1 + app/components/dashboard/Navbar.tsx | 17 ++- app/components/dashboard/Settings/Profile.tsx | 144 ++++++++++-------- .../dashboard/Settings/ResetPassword.tsx | 124 ++++++++++----- .../dashboard/Settings/WakaTimeKey.tsx | 120 +++++++++------ app/d/settings/page.tsx | 27 +--- 7 files changed, 288 insertions(+), 181 deletions(-) diff --git a/app/api/auth/forgot-password/route.ts b/app/api/auth/forgot-password/route.ts index f8a5996..3e9b638 100644 --- a/app/api/auth/forgot-password/route.ts +++ b/app/api/auth/forgot-password/route.ts @@ -1,6 +1,8 @@ import { NextResponse } from "next/server"; import { prisma } from "@/app/lib/prisma"; import { recaptcha } from "@/app/lib/recaptcha"; +import { NODE_MAILER_USER, transporter } from "@/app/lib/smtp/nodemailer"; +import emailTemplate from "@/app/lib/smtp/template"; export async function POST(req: Request) { const { email, token } = await req.json(); @@ -26,7 +28,7 @@ export async function POST(req: Request) { return NextResponse.json({ success: true }); } - const expiresAt = new Date(Date.now() + 60 * 60 * 1000); + const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour from now const { token: resetToken } = await prisma.passwordResetToken.create({ data: { user_id: user.id, expires_at: expiresAt }, @@ -35,7 +37,37 @@ export async function POST(req: Request) { const resetUrl = `${process.env.NEXTAUTH_URL}/reset-password?token=${resetToken}`; - console.info(`Password reset link for ${email}: ${resetUrl}`); + transporter.sendMail({ + from: `Do Not Reply <${NODE_MAILER_USER}>`, + to: user.email, + subject: "Reset your password", + html: emailTemplate({ + title: "Reset your password", + bodyHtml: ` +

Hi ${user.name},

+

We received a request to reset your Devpulse password. Click the button below to choose a new one.

+ +

+ Or copy and paste this link into your browser:
+ ${resetUrl} +

+

+ This link will expire in 1 hour. If you didn't request a password reset, you can safely ignore this email. Your password will remain unchanged. +

+
+

+ Have a suggestion or ran into an issue? Please don't hesitate to reach out at + hallofcodes.org. + We'd love to hear from you. +

+
+ `, + }), + }); return NextResponse.json({ success: true }); } diff --git a/app/components/common/NavProfileDropdown.tsx b/app/components/common/NavProfileDropdown.tsx index 0231ff5..32bb460 100644 --- a/app/components/common/NavProfileDropdown.tsx +++ b/app/components/common/NavProfileDropdown.tsx @@ -141,6 +141,7 @@ export default function NavProfileDropdown({ Settings + {type === "navbar" && (