Skip to content

Commit 76d560e

Browse files
committed
feat: stabilize chat realtime, dashboard insights, and account flows
Refines PR hallofcodes#57 into a single clean commit covering chat, dashboard, landing, settings, and migration updates. Includes reviewer follow-ups for BOM cleanup, component fixes, migration readability, and history hygiene.
1 parent 5866a0c commit 76d560e

42 files changed

Lines changed: 4228 additions & 1079 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/(auth)/login/page.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,27 @@ export default async function Login(props: {
6363
: undefined;
6464

6565
return (
66-
<div className="min-h-screen flex bg-[#0a0a1a] text-white">
66+
<div className="min-h-screen flex bg-[#0a0a1a] text-white relative">
67+
<Link
68+
href="/"
69+
className="absolute top-5 left-5 sm:top-6 sm:left-6 z-40 inline-flex items-center gap-2 text-sm text-gray-400 hover:text-white transition-colors"
70+
>
71+
<svg
72+
className="w-4 h-4"
73+
fill="none"
74+
viewBox="0 0 24 24"
75+
stroke="currentColor"
76+
>
77+
<path
78+
strokeLinecap="round"
79+
strokeLinejoin="round"
80+
strokeWidth={2}
81+
d="M15 19l-7-7 7-7"
82+
/>
83+
</svg>
84+
Back
85+
</Link>
86+
6787
{/* Left Side - Visual / Branding */}
6888
<div className="hidden lg:flex lg:w-1/2 relative flex-col justify-between p-12 md:p-16 xl:p-24 border-r border-white/5 bg-gradient-to-br from-[#0a0a1a] to-[#0a0a1a] overflow-hidden">
6989
{/* Background elements */}

app/(auth)/signup/page.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,27 @@ export default async function Signup(props: {
5050
: undefined;
5151

5252
return (
53-
<div className="min-h-screen flex bg-[#0a0a1a] text-white">
53+
<div className="min-h-screen flex bg-[#0a0a1a] text-white relative">
54+
<Link
55+
href="/"
56+
className="absolute top-5 left-5 sm:top-6 sm:left-6 z-40 inline-flex items-center gap-2 text-sm text-gray-400 hover:text-white transition-colors"
57+
>
58+
<svg
59+
className="w-4 h-4"
60+
fill="none"
61+
viewBox="0 0 24 24"
62+
stroke="currentColor"
63+
>
64+
<path
65+
strokeLinecap="round"
66+
strokeLinejoin="round"
67+
strokeWidth={2}
68+
d="M15 19l-7-7 7-7"
69+
/>
70+
</svg>
71+
Back
72+
</Link>
73+
5474
{/* Left Side - Visual / Branding */}
5575
<div className="hidden lg:flex lg:w-1/2 relative flex-col justify-between p-12 md:p-16 xl:p-24 border-r border-white/5 bg-gradient-to-br from-[#0a0a1a] to-[#0a0a1a] overflow-hidden">
5676
{/* Background elements */}

app/(user)/dashboard/settings/page.tsx

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,72 @@
11
import { Metadata } from "next";
22
import UserProfile from "@/app/components/dashboard/Settings/Profile";
33
import ResetPassword from "@/app/components/dashboard/Settings/ResetPassword";
4+
import WakaTimeKey from "@/app/components/dashboard/Settings/WakaTimeKey";
45
import { getUserWithProfile } from "@/app/lib/supabase/help/user";
56
import { redirect } from "next/navigation";
67

78
export const metadata: Metadata = {
89
title: "Settings - DevPulse",
910
};
1011

11-
export default async function LeaderboardsPage() {
12-
const { user } = await getUserWithProfile();
12+
export default async function SettingsPage() {
13+
const { user, profile } = await getUserWithProfile();
1314
if (!user) return redirect("/login?from=/dashboard/settings");
1415

16+
const hasWakaKey = Boolean(profile?.wakatime_api_key);
17+
const maskedWakaKey = profile?.wakatime_api_key
18+
? `${profile.wakatime_api_key.slice(0, 8)}...${profile.wakatime_api_key.slice(-4)}`
19+
: null;
20+
1521
return (
16-
<div className="p-6 md:p-8 space-y-6">
17-
<div>
18-
<h1 className="text-2xl font-bold text-white">Settings</h1>
19-
<p className="text-sm text-gray-600">
20-
Manage your account settings and including your WakaTime API key.
21-
</p>
22+
<div className="p-4 md:p-6 space-y-4 max-w-7xl mx-auto">
23+
<div className="glass-card p-4 md:p-5 border-t-4 border-indigo-500/50">
24+
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
25+
<div>
26+
<h1 className="text-xl md:text-2xl font-bold text-white tracking-tight">
27+
Account Settings
28+
</h1>
29+
<p className="text-xs md:text-sm text-gray-400 mt-1">
30+
Manage profile details, WakaTime connection, and account security.
31+
</p>
32+
</div>
33+
34+
<div className="flex items-center gap-2 text-[11px] uppercase tracking-wider">
35+
<span
36+
className={`px-2 py-1 rounded-full border font-semibold ${
37+
hasWakaKey
38+
? "border-emerald-500/30 bg-emerald-500/10 text-emerald-300"
39+
: "border-amber-500/30 bg-amber-500/10 text-amber-300"
40+
}`}
41+
>
42+
{hasWakaKey ? "WakaTime Connected" : "WakaTime Not Connected"}
43+
</span>
44+
</div>
45+
</div>
2246
</div>
2347

2448
{user && (
25-
<>
26-
<UserProfile user={user} />
27-
<ResetPassword user={user} />
28-
</>
49+
<div className="grid grid-cols-1 xl:grid-cols-3 gap-4 items-start">
50+
<div className="xl:col-span-2 space-y-4">
51+
<UserProfile user={user} />
52+
<WakaTimeKey hasKey={hasWakaKey} maskedKey={maskedWakaKey} />
53+
</div>
54+
55+
<div className="xl:col-span-1 space-y-4">
56+
<ResetPassword user={user} />
57+
58+
<div className="glass-card p-5 border-t-4 border-indigo-500/40">
59+
<h3 className="text-xs font-semibold text-indigo-300 uppercase tracking-widest mb-3">
60+
Best Practices
61+
</h3>
62+
<div className="space-y-2 text-xs md:text-sm text-gray-400 leading-relaxed">
63+
<p>Rotate API keys periodically and avoid sharing them in screenshots.</p>
64+
<p>Use a strong password and reset it immediately if account activity looks unusual.</p>
65+
<p>After changing your API key, run a dashboard sync to refresh your metrics.</p>
66+
</div>
67+
</div>
68+
</div>
69+
</div>
2970
)}
3071
</div>
3172
);

app/api/wakatime/sync/route.ts

Lines changed: 114 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,72 @@ import { NextResponse } from "next/server";
22
import { createClient } from "../../../lib/supabase/server";
33
import { getUserWithProfile } from "@/app/lib/supabase/help/user";
44

5+
function formatDateYMD(date: Date) {
6+
const y = date.getFullYear();
7+
const m = String(date.getMonth() + 1).padStart(2, "0");
8+
const d = String(date.getDate()).padStart(2, "0");
9+
return `${y}-${m}-${d}`;
10+
}
11+
12+
function toDateKey(value: string) {
13+
return value.slice(0, 10);
14+
}
15+
16+
type DailyStat = {
17+
date: string;
18+
total_seconds: number;
19+
};
20+
21+
function buildSnapshotMetrics(dailyStats: DailyStat[]) {
22+
const normalized = [...dailyStats]
23+
.map((entry) => ({
24+
date: toDateKey(entry.date),
25+
total_seconds: Math.max(0, Math.floor(entry.total_seconds || 0)),
26+
}))
27+
.sort((a, b) => a.date.localeCompare(b.date));
28+
29+
const last7 = normalized.slice(-7);
30+
const totalSeconds7d = last7.reduce((sum, day) => sum + day.total_seconds, 0);
31+
const activeDays7d = last7.filter((day) => day.total_seconds > 0).length;
32+
33+
const activeByDay = normalized.map((day) => day.total_seconds > 0);
34+
const activeDays = activeByDay.filter(Boolean).length;
35+
const consistencyPercent =
36+
normalized.length > 0
37+
? Math.round((activeDays / normalized.length) * 100)
38+
: 0;
39+
40+
let bestStreak = 0;
41+
let runningStreak = 0;
42+
for (const isActive of activeByDay) {
43+
runningStreak = isActive ? runningStreak + 1 : 0;
44+
if (runningStreak > bestStreak) bestStreak = runningStreak;
45+
}
46+
47+
let currentStreak = 0;
48+
for (let i = activeByDay.length - 1; i >= 0; i -= 1) {
49+
if (!activeByDay[i]) break;
50+
currentStreak += 1;
51+
}
52+
53+
const peakDay = last7.reduce(
54+
(max, day) => (day.total_seconds > max.total_seconds ? day : max),
55+
{ date: "", total_seconds: 0 },
56+
);
57+
58+
return {
59+
totalSeconds7d,
60+
activeDays7d,
61+
consistencyPercent,
62+
currentStreak,
63+
bestStreak,
64+
peakDayDate: peakDay.date || null,
65+
peakDaySeconds: peakDay.total_seconds,
66+
};
67+
}
68+
569
export async function GET(request: Request) {
70+
const CONSISTENCY_DAYS = 365;
671
const supabase = await createClient();
772
const { user, profile } = await getUserWithProfile();
873
const { searchParams } = new URL(request.url);
@@ -45,21 +110,29 @@ export async function GET(request: Request) {
45110

46111
const now = new Date();
47112
const sixHours = 6 * 60 * 60 * 1000;
113+
const existingDailyStats = Array.isArray(existing?.daily_stats)
114+
? existing.daily_stats
115+
: [];
48116

49117
if (existing?.last_fetched_at) {
50118
const lastFetch = new Date(existing.last_fetched_at).getTime();
51-
if (now.getTime() - lastFetch < sixHours) {
119+
if (
120+
now.getTime() - lastFetch < sixHours &&
121+
existingDailyStats.length >= CONSISTENCY_DAYS
122+
) {
52123
return NextResponse.json({ success: true, data: existing });
53124
}
54125
}
55126
}
56127

57128
// Fetch from WakaTime API endpoints
58129
const endDate = new Date();
130+
endDate.setHours(0, 0, 0, 0);
59131
const startDate = new Date();
60-
startDate.setDate(endDate.getDate() - 6);
61-
const endStr = endDate.toISOString().split("T")[0];
62-
const startStr = startDate.toISOString().split("T")[0];
132+
startDate.setHours(0, 0, 0, 0);
133+
startDate.setDate(endDate.getDate() - (CONSISTENCY_DAYS - 1));
134+
const endStr = formatDateYMD(endDate);
135+
const startStr = formatDateYMD(startDate);
63136

64137
const authHeader = `Basic ${Buffer.from(profile$.wakatime_api_key).toString("base64")}`;
65138

@@ -94,11 +167,17 @@ export async function GET(request: Request) {
94167
range: { date: string };
95168
grand_total: { total_seconds: number };
96169
}) => ({
97-
date: day.range.date,
98-
total_seconds: day.grand_total.total_seconds,
170+
date: toDateKey(day.range.date),
171+
total_seconds: Math.floor(day.grand_total.total_seconds || 0),
99172
}),
100173
);
101174

175+
const snapshotMetrics = buildSnapshotMetrics(daily_stats);
176+
const topLanguage =
177+
Array.isArray(wakaStats.languages) && wakaStats.languages.length > 0
178+
? wakaStats.languages[0]
179+
: null;
180+
102181
if (apiKey) {
103182
const { error } = await supabase
104183
.from("profiles")
@@ -158,6 +237,35 @@ export async function GET(request: Request) {
158237
projects: projectsResult?.projects || [],
159238
};
160239

240+
const { error: snapshotError } = await supabase
241+
.from("user_dashboard_snapshots")
242+
.upsert(
243+
{
244+
user_id: user.id,
245+
snapshot_date: endStr,
246+
total_seconds_7d: snapshotMetrics.totalSeconds7d,
247+
active_days_7d: snapshotMetrics.activeDays7d,
248+
consistency_percent: snapshotMetrics.consistencyPercent,
249+
current_streak: snapshotMetrics.currentStreak,
250+
best_streak: snapshotMetrics.bestStreak,
251+
peak_day: snapshotMetrics.peakDayDate,
252+
peak_day_seconds: snapshotMetrics.peakDaySeconds,
253+
top_language: topLanguage?.name || null,
254+
top_language_percent:
255+
typeof topLanguage?.percent === "number"
256+
? Number(topLanguage.percent.toFixed(2))
257+
: null,
258+
updated_at: new Date().toISOString(),
259+
},
260+
{
261+
onConflict: "user_id,snapshot_date",
262+
},
263+
);
264+
265+
if (snapshotError) {
266+
console.error("Failed to upsert user dashboard snapshot", snapshotError);
267+
}
268+
161269
return NextResponse.json({
162270
success: !!statsResult && !statsError && !projectsError,
163271
data: mergedResult,

app/components/BrowserCheck.tsx

Lines changed: 0 additions & 27 deletions
This file was deleted.

0 commit comments

Comments
 (0)