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"
>
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 (
router.push(href)}
- className="flex items-center gap-2 text-sm font-medium text-gray-500 hover:text-indigo-600 transition-colors mb-6 group w-fit"
+ className="flex items-center gap-2 text-sm font-medium text-gray-500 hover:text-blue-600 transition-colors mb-6 group w-fit"
>
-
+
Back
diff --git a/app/components/leaderboard/InviteFriendsButton.tsx b/app/components/leaderboard/InviteFriendsButton.tsx
index a52e66e..927339a 100644
--- a/app/components/leaderboard/InviteFriendsButton.tsx
+++ b/app/components/leaderboard/InviteFriendsButton.tsx
@@ -29,7 +29,7 @@ export default function InviteFriendsButton({
return (
Invite Friends
diff --git a/app/components/leaderboard/LeaderboardStats.tsx b/app/components/leaderboard/LeaderboardStats.tsx
index c495f51..6d31568 100644
--- a/app/components/leaderboard/LeaderboardStats.tsx
+++ b/app/components/leaderboard/LeaderboardStats.tsx
@@ -115,7 +115,7 @@ export default function LeaderboardStats({
return (
diff --git a/app/components/leaderboard/LeaderboardTable.tsx b/app/components/leaderboard/LeaderboardTable.tsx
index d6e1ab3..ee6ea0d 100644
--- a/app/components/leaderboard/LeaderboardTable.tsx
+++ b/app/components/leaderboard/LeaderboardTable.tsx
@@ -45,7 +45,7 @@ function LeaderboardPodium({
return (
{/* Minimal Background Glow based on rank */}
{/* Background Progress Bar */}
diff --git a/app/d/kanban/page.tsx b/app/d/kanban/page.tsx
index b44ee82..b2e2230 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;
@@ -68,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" &&
@@ -78,22 +78,6 @@ function isKanbanProject(value: unknown): value is KanbanProject {
);
}
-const PROJECT_COLORS = [
- { value: "indigo", label: "Indigo" },
- { value: "cyan", label: "Cyan" },
- { value: "emerald", label: "Emerald" },
- { value: "amber", label: "Amber" },
- { value: "rose", label: "Rose" },
-];
-
-const COLOR_STYLES: Record
= {
- indigo: "from-indigo-500/25 to-violet-500/10 border-indigo-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 +148,7 @@ export default function Kanban() {
const [projectForm, setProjectForm] = useState({
name: "",
description: "",
- wakatimeProjectName: "",
- color: "indigo",
+ wakatime_project_name: "",
});
const load = useCallback(async () => {
@@ -291,9 +274,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 +296,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 +339,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 +399,7 @@ export default function Kanban() {
setProjectForm({
name: "",
description: "",
- wakatimeProjectName: "",
- color: "indigo",
+ wakatime_project_name: "",
});
setProjectModalOpen(false);
toast.success("Kanban project created.");
@@ -430,111 +414,89 @@ export default function Kanban() {
}
return (
-
- {/* Sticky header — title + actions only */}
-
-
-
-
-
- Project Kanban
-
-
Boards
-
-
setProjectModalOpen(true)}
- className="rounded-xl border border-gray-200 bg-white px-3 py-2 text-sm font-medium text-gray-600 shadow-sm transition hover:border-indigo-300 hover:text-indigo-600"
- >
- + New Project
-
-
-
+
+ {/* 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.
+
+
setProjectModalOpen(true)}
+ className="rounded-xl border border-gray-200 bg-white px-3 py-2 text-sm font-medium text-gray-600 shadow-sm transition hover:border-blue-300 hover:text-blue-600"
+ >
+
+
+
-
-
- Active Project
-
- setSelectedProject(event.target.value)}
- className="input-field h-12 w-full"
- >
- {projects.length === 0 && (
- No projects yet
- )}
- {projects.map((project) => (
-
- {project.name}
-
- ))}
-
-
+
+
+ Active Project
+
+ setSelectedProject(event.target.value)}
+ className="input-field h-12 w-full"
+ >
+ {projects.length === 0 && (
+ No projects yet
+ )}
+ {projects.map((project) => (
+
+ {project.name}
+
+ ))}
+
+
+
{currentProject ? (
-
+
{currentProject.name}
- {currentProject.description || "No project brief yet."}
+ {currentProject.description}
-
- {currentProject.color}
-
@@ -565,127 +527,132 @@ export default function Kanban() {
/>
)}
-
-
-
setShowRecentIssues((v) => !v)}
- >
-
-
- Recent Issues
-
-
- Latest cards for the active project.
-
-
-
- {showRecentIssues ? "▲" : "▼"}
-
-
-
-
- {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 */}
-
-
+
+
setShowRecentIssues((v) => !v)}
+ >
-
Boards
+
+ Recent Issues
+
- {currentProject
- ? "Delivery lanes for the active project."
- : "Create a project first to open a board."}
+ Latest cards for the active project.
- {currentProject?.wakatime_project_name ? (
-
- Bound to {currentProject.wakatime_project_name}
-
- ) : null}
-
+
+ {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 +688,11 @@ export default function Kanban() {
/>
setProjectForm((prev) => ({
...prev,
- wakatimeProjectName: event.target.value,
+ wakatime_project_name: event.target.value,
}))
}
className="input-field w-full"
@@ -737,33 +704,6 @@ export default function Kanban() {
))}
-
-
-
- Accent
-
-
- {PROJECT_COLORS.map((color) => (
-
- setProjectForm((prev) => ({
- ...prev,
- color: color.value,
- }))
- }
- className={`rounded-xl border px-3 py-2 text-xs font-semibold transition-all ${
- projectForm.color === color.value
- ? "border-gray-400 bg-gray-100 text-gray-900"
- : "border-gray-200 bg-gray-50 text-gray-500"
- }`}
- >
- {color.label}
-
- ))}
-
-
@@ -894,7 +834,9 @@ function OverviewCard({
}) {
return (
-
{label}
+
+ {label}
+
{value}
{sub}
@@ -951,13 +893,13 @@ function Column({
}) {
const { setNodeRef } = useDroppable({
id: column.id,
- data: { columnId: column.id },
+ data: { column_id: column.id },
});
return (
@@ -970,7 +912,7 @@ function Column({
Add
@@ -992,10 +934,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 (
-
+
+
Leaderboards
@@ -23,12 +23,10 @@ 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/d/settings/page.tsx b/app/d/settings/page.tsx
index 67e13f1..1b813d7 100644
--- a/app/d/settings/page.tsx
+++ b/app/d/settings/page.tsx
@@ -13,38 +13,13 @@ 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 (
-
-
-
-
-
- Account Settings
-
-
- Manage profile details, WakaTime connection, and account security.
-
-
-
-
-
- {hasWakaKey ? "WakaTime Connected" : "WakaTime Not Connected"}
-
-
-
-
-
+
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 12b05df..16207bf 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.email_verified ||
+ !token.wakatime_api_key)
) {
const dbUser = await prisma.user.findUnique({
where: { id: token.sub },
- select: { role: true, emailVerified: true },
+ select: { role: true, email_verified: true, wakatime_api_key: true },
});
if (dbUser) {
token.role = dbUser.role;
- token.emailVerified = dbUser.emailVerified;
+ token.email_verified = dbUser.email_verified;
+ token.wakatime_api_key = dbUser.wakatime_api_key;
}
}
@@ -91,22 +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.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: {},
});
@@ -120,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/auth/verify-email.ts b/app/lib/auth/verify-email.ts
new file mode 100644
index 0000000..3bcfc78
--- /dev/null
+++ b/app/lib/auth/verify-email.ts
@@ -0,0 +1,68 @@
+import { prisma } from "@/app/lib/prisma";
+import { NextResponse } from "next/server";
+import { NODE_MAILER_USER, transporter } from "../smtp/nodemailer";
+import emailTemplate from "../smtp/template";
+import crypto from "crypto";
+
+/**
+ * Verifies the email address for a given user.
+ *
+ * @param email The email address to verify.
+ * @returns A JSON response indicating success or failure.
+ */
+export default async function verifyEmail(email: string) {
+ try {
+ 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() + 6 * 60 * 60 * 1000); // 6 hours from now
+
+ 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: 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 a9de4a8..636b1f7 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;
}
@@ -79,25 +78,22 @@ export async function getKanbanProjectAccess(
export async function getColumnAccess(
userId: string,
columnId: string,
-): Promise<
- | {
- columnId: string;
- projectId: string;
- projectName: string;
- }
- | null
-> {
+): Promise<{
+ column_id: string;
+ project_id: string;
+ project_name: string;
+} | null> {
const rows = await prisma.$queryRaw<
Array<{
- columnId: string;
- projectId: string;
- projectName: 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
@@ -112,22 +108,19 @@ export async function getColumnAccess(
export async function getIssueAccess(
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
@@ -153,13 +146,14 @@ export async function getNextIssueKey(projectId: string, projectName: 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")}`;
}
@@ -173,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
@@ -221,7 +214,7 @@ export async function getKanbanData(userId: string) {
ORDER BY i.position ASC, i.created_at ASC
`,
prisma.userProjects.findUnique({
- where: { userId },
+ where: { user_id: userId },
select: { projects: true },
}),
]);
@@ -229,7 +222,9 @@ export async function getKanbanData(userId: 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 +241,6 @@ export async function getKanbanData(userId: string) {
name: project.name,
description: project.description ?? "",
wakatime_project_name: project.wakatime_project_name ?? "",
- color: project.color ?? "indigo",
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 b5ab9df..b3c5e0b 100644
--- a/app/lib/proxy/auth.ts
+++ b/app/lib/proxy/auth.ts
@@ -14,9 +14,12 @@ 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.wakatime_api_key && pathname.startsWith("/d")) {
+ return NextResponse.redirect(new URL("/verify-wakatime", req.url));
+ }
}
const authRoutes = [
@@ -27,13 +30,19 @@ 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.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) {
+ if (
+ (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/recaptcha.ts b/app/lib/recaptcha.ts
index 41c937c..9e53915 100644
--- a/app/lib/recaptcha.ts
+++ b/app/lib/recaptcha.ts
@@ -3,11 +3,14 @@ import { RecaptchaEnterpriseServiceClient } from "@google-cloud/recaptcha-enterp
const projectId = process.env.GOOGLE_CLOUD_PROJECT || "";
const siteKey = process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY || "";
-const credentials = JSON.parse(
- Buffer.from(process.env.GOOGLE_APPLICATION_CREDENTIALS!, "base64").toString(
- "utf-8",
- ),
-);
+const credentials = process.env.GOOGLE_APPLICATION_CREDENTIALS
+ ? JSON.parse(
+ Buffer.from(
+ process.env.GOOGLE_APPLICATION_CREDENTIALS,
+ "base64",
+ ).toString("utf-8"),
+ )
+ : "";
const client = new RecaptchaEnterpriseServiceClient({ credentials });
async function recaptcha(token: string, action: string): Promise
{
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/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 91da57b..fc59469 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.
@@ -161,17 +169,21 @@ 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
) {
- return { status: 200, success: true, data: existing };
+ return {
+ status: 200,
+ success: true,
+ data: serializeBigInts(existing),
+ };
}
}
}
@@ -220,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),
}),
]);
@@ -248,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);
@@ -273,6 +285,6 @@ export async function syncWakatimeData({
return {
status: 200,
success: true,
- data: mergedResult,
+ data: serializeBigInts(mergedResult),
};
}
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() {
{/* 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..477706a 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 "N/A";
+
+ 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 "N/A";
+ }
}
+/**
+ * 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 c77bb3e..e2ea773 100644
--- a/next-auth.d.ts
+++ b/next-auth.d.ts
@@ -6,10 +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;
+ wakatime_api_key?: string | null;
};
}
@@ -23,6 +24,7 @@ declare module "next-auth/jwt" {
interface JWT {
id?: string;
role?: string;
- emailVerified?: Date | null;
+ email_verified?: Date | null;
+ wakatime_api_key?: string | null;
}
}
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",
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/20260823103009_init/migration.sql
similarity index 97%
rename from prisma/migrations/20260728043534_init/migration.sql
rename to prisma/migrations/20260823103009_init/migration.sql
index 2bb5c57..acfdfd5 100644
--- a/prisma/migrations/20260728043534_init/migration.sql
+++ b/prisma/migrations/20260823103009_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,13 @@ 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,
`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 +293,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..5276fa7 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,95 @@ 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?
+ 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")
}
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",
],
};