Skip to content

Commit b04701d

Browse files
committed
feat: stabilize chat realtime, dashboard insights, and account flows
Refines PR #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 b04701d

Some content is hidden

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

45 files changed

+4298
-1070
lines changed

app/(auth)/login/page.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import Link from "next/link";
22
import Image from "next/image";
33
import LoginForm from "@/app/components/auth/LoginForm";
44
import { Metadata } from "next";
5+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
6+
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
57

68
export const metadata: Metadata = {
79
title: "Login - DevPulse",
@@ -63,7 +65,15 @@ export default async function Login(props: {
6365
: undefined;
6466

6567
return (
66-
<div className="min-h-screen flex bg-[#0a0a1a] text-white">
68+
<div className="min-h-screen flex bg-[#0a0a1a] text-white relative">
69+
<Link
70+
href="/"
71+
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"
72+
>
73+
<FontAwesomeIcon icon={faChevronLeft} className="w-4 h-4" />
74+
Back
75+
</Link>
76+
6777
{/* Left Side - Visual / Branding */}
6878
<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">
6979
{/* Background elements */}

app/(auth)/signup/page.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import Link from "next/link";
22
import Image from "next/image";
33
import SignupForm from "@/app/components/auth/SignupForm";
44
import { Metadata } from "next";
5+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
6+
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
57

68
export const metadata: Metadata = {
79
title: "Sign Up - DevPulse",
@@ -50,7 +52,15 @@ export default async function Signup(props: {
5052
: undefined;
5153

5254
return (
53-
<div className="min-h-screen flex bg-[#0a0a1a] text-white">
55+
<div className="min-h-screen flex bg-[#0a0a1a] text-white relative">
56+
<Link
57+
href="/"
58+
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"
59+
>
60+
<FontAwesomeIcon icon={faChevronLeft} className="w-4 h-4" />
61+
Back
62+
</Link>
63+
5464
{/* Left Side - Visual / Branding */}
5565
<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">
5666
{/* Background elements */}

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

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,61 @@
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">
56+
<ResetPassword user={user} />
57+
</div>
58+
</div>
2959
)}
3060
</div>
3161
);

app/api/wakatime/sync/route.ts

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import { NextResponse } from "next/server";
22
import { createClient } from "../../../lib/supabase/server";
33
import { getUserWithProfile } from "@/app/lib/supabase/help/user";
4+
import {
5+
buildSnapshotMetrics,
6+
formatDateYMD,
7+
toDateKey,
8+
} from "@/app/utils/wakatime";
49

510
export async function GET(request: Request) {
11+
const CONSISTENCY_DAYS = 365;
612
const supabase = await createClient();
713
const { user, profile } = await getUserWithProfile();
814
const { searchParams } = new URL(request.url);
@@ -45,21 +51,29 @@ export async function GET(request: Request) {
4551

4652
const now = new Date();
4753
const sixHours = 6 * 60 * 60 * 1000;
54+
const existingDailyStats = Array.isArray(existing?.daily_stats)
55+
? existing.daily_stats
56+
: [];
4857

4958
if (existing?.last_fetched_at) {
5059
const lastFetch = new Date(existing.last_fetched_at).getTime();
51-
if (now.getTime() - lastFetch < sixHours) {
60+
if (
61+
now.getTime() - lastFetch < sixHours &&
62+
existingDailyStats.length >= CONSISTENCY_DAYS
63+
) {
5264
return NextResponse.json({ success: true, data: existing });
5365
}
5466
}
5567
}
5668

5769
// Fetch from WakaTime API endpoints
5870
const endDate = new Date();
71+
endDate.setHours(0, 0, 0, 0);
5972
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];
73+
startDate.setHours(0, 0, 0, 0);
74+
startDate.setDate(endDate.getDate() - (CONSISTENCY_DAYS - 1));
75+
const endStr = formatDateYMD(endDate);
76+
const startStr = formatDateYMD(startDate);
6377

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

@@ -94,11 +108,17 @@ export async function GET(request: Request) {
94108
range: { date: string };
95109
grand_total: { total_seconds: number };
96110
}) => ({
97-
date: day.range.date,
98-
total_seconds: day.grand_total.total_seconds,
111+
date: toDateKey(day.range.date),
112+
total_seconds: Math.floor(day.grand_total.total_seconds || 0),
99113
}),
100114
);
101115

116+
const snapshotMetrics = buildSnapshotMetrics(daily_stats);
117+
const topLanguage =
118+
Array.isArray(wakaStats.languages) && wakaStats.languages.length > 0
119+
? wakaStats.languages[0]
120+
: null;
121+
102122
if (apiKey) {
103123
const { error } = await supabase
104124
.from("profiles")
@@ -158,6 +178,35 @@ export async function GET(request: Request) {
158178
projects: projectsResult?.projects || [],
159179
};
160180

181+
const { error: snapshotError } = await supabase
182+
.from("user_dashboard_snapshots")
183+
.upsert(
184+
{
185+
user_id: user.id,
186+
snapshot_date: endStr,
187+
total_seconds_7d: snapshotMetrics.totalSeconds7d,
188+
active_days_7d: snapshotMetrics.activeDays7d,
189+
consistency_percent: snapshotMetrics.consistencyPercent,
190+
current_streak: snapshotMetrics.currentStreak,
191+
best_streak: snapshotMetrics.bestStreak,
192+
peak_day: snapshotMetrics.peakDayDate,
193+
peak_day_seconds: snapshotMetrics.peakDaySeconds,
194+
top_language: topLanguage?.name || null,
195+
top_language_percent:
196+
typeof topLanguage?.percent === "number"
197+
? Number(topLanguage.percent.toFixed(2))
198+
: null,
199+
updated_at: new Date().toISOString(),
200+
},
201+
{
202+
onConflict: "user_id,snapshot_date",
203+
},
204+
);
205+
206+
if (snapshotError) {
207+
console.error("Failed to upsert user dashboard snapshot", snapshotError);
208+
}
209+
161210
return NextResponse.json({
162211
success: !!statsResult && !statsError && !projectsError,
163212
data: mergedResult,

app/components/BrowserCheck.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@ export default function BrowserCheck() {
2424
}, []);
2525

2626
return null;
27-
}
27+
}

0 commit comments

Comments
 (0)