Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ next-env.d.ts
# supabase
supabase/*
!supabase/migrations
supabase/.temp/
.temp/


yarn.lock
Expand Down
22 changes: 21 additions & 1 deletion app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,27 @@ export default async function Login(props: {
: undefined;

return (
<div className="min-h-screen flex bg-[#0a0a1a] text-white">
<div className="min-h-screen flex bg-[#0a0a1a] text-white relative">
<Link
href="/"
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"
>
<svg
className="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Back
</Link>

{/* Left Side - Visual / Branding */}
<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">
{/* Background elements */}
Expand Down
22 changes: 21 additions & 1 deletion app/(auth)/signup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,27 @@ export default async function Signup(props: {
: undefined;

return (
<div className="min-h-screen flex bg-[#0a0a1a] text-white">
<div className="min-h-screen flex bg-[#0a0a1a] text-white relative">
<Link
href="/"
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"
>
<svg
className="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Back
</Link>

{/* Left Side - Visual / Branding */}
<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">
{/* Background elements */}
Expand Down
65 changes: 53 additions & 12 deletions app/(user)/dashboard/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,72 @@
import { Metadata } from "next";
import UserProfile from "@/app/components/dashboard/Settings/Profile";
import ResetPassword from "@/app/components/dashboard/Settings/ResetPassword";
import WakaTimeKey from "@/app/components/dashboard/Settings/WakaTimeKey";
import { getUserWithProfile } from "@/app/lib/supabase/help/user";
import { redirect } from "next/navigation";

export const metadata: Metadata = {
title: "Settings - DevPulse",
};

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

const hasWakaKey = Boolean(profile?.wakatime_api_key);
const maskedWakaKey = profile?.wakatime_api_key
? `${profile.wakatime_api_key.slice(0, 8)}...${profile.wakatime_api_key.slice(-4)}`
: null;

return (
<div className="p-6 md:p-8 space-y-6">
<div>
<h1 className="text-2xl font-bold text-white">Settings</h1>
<p className="text-sm text-gray-600">
Manage your account settings and including your WakaTime API key.
</p>
<div className="p-4 md:p-6 space-y-4 max-w-7xl mx-auto">
<div className="glass-card p-4 md:p-5 border-t-4 border-indigo-500/50">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-xl md:text-2xl font-bold text-white tracking-tight">
Account Settings
</h1>
<p className="text-xs md:text-sm text-gray-400 mt-1">
Manage profile details, WakaTime connection, and account security.
</p>
</div>

<div className="flex items-center gap-2 text-[11px] uppercase tracking-wider">
<span
className={`px-2 py-1 rounded-full border font-semibold ${
hasWakaKey
? "border-emerald-500/30 bg-emerald-500/10 text-emerald-300"
: "border-amber-500/30 bg-amber-500/10 text-amber-300"
}`}
>
{hasWakaKey ? "WakaTime Connected" : "WakaTime Not Connected"}
</span>
</div>
</div>
</div>

{user && (
<>
<UserProfile user={user} />
<ResetPassword user={user} />
</>
<div className="grid grid-cols-1 xl:grid-cols-3 gap-4 items-start">
<div className="xl:col-span-2 space-y-4">
<UserProfile user={user} />
<WakaTimeKey hasKey={hasWakaKey} maskedKey={maskedWakaKey} />
</div>

<div className="xl:col-span-1 space-y-4">
<ResetPassword user={user} />

<div className="glass-card p-5 border-t-4 border-indigo-500/40">
<h3 className="text-xs font-semibold text-indigo-300 uppercase tracking-widest mb-3">
Best Practices
</h3>
<div className="space-y-2 text-xs md:text-sm text-gray-400 leading-relaxed">
<p>Rotate API keys periodically and avoid sharing them in screenshots.</p>
<p>Use a strong password and reset it immediately if account activity looks unusual.</p>
<p>After changing your API key, run a dashboard sync to refresh your metrics.</p>
</div>
</div>
</div>
</div>
)}
</div>
);
Expand Down
120 changes: 114 additions & 6 deletions app/api/wakatime/sync/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,72 @@ import { NextResponse } from "next/server";
import { createClient } from "../../../lib/supabase/server";
import { getUserWithProfile } from "@/app/lib/supabase/help/user";

function formatDateYMD(date: Date) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, "0");
const d = String(date.getDate()).padStart(2, "0");
return `${y}-${m}-${d}`;
}

function toDateKey(value: string) {
return value.slice(0, 10);
}

type DailyStat = {
date: string;
total_seconds: number;
};

function buildSnapshotMetrics(dailyStats: DailyStat[]) {
const normalized = [...dailyStats]
.map((entry) => ({
date: toDateKey(entry.date),
total_seconds: Math.max(0, Math.floor(entry.total_seconds || 0)),
}))
.sort((a, b) => a.date.localeCompare(b.date));

const last7 = normalized.slice(-7);
const totalSeconds7d = last7.reduce((sum, day) => sum + day.total_seconds, 0);
const activeDays7d = last7.filter((day) => day.total_seconds > 0).length;

const activeByDay = normalized.map((day) => day.total_seconds > 0);
const activeDays = activeByDay.filter(Boolean).length;
const consistencyPercent =
normalized.length > 0
? Math.round((activeDays / normalized.length) * 100)
: 0;

let bestStreak = 0;
let runningStreak = 0;
for (const isActive of activeByDay) {
runningStreak = isActive ? runningStreak + 1 : 0;
if (runningStreak > bestStreak) bestStreak = runningStreak;
}

let currentStreak = 0;
for (let i = activeByDay.length - 1; i >= 0; i -= 1) {
if (!activeByDay[i]) break;
currentStreak += 1;
}

const peakDay = last7.reduce(
(max, day) => (day.total_seconds > max.total_seconds ? day : max),
{ date: "", total_seconds: 0 },
);

return {
totalSeconds7d,
activeDays7d,
consistencyPercent,
currentStreak,
bestStreak,
peakDayDate: peakDay.date || null,
peakDaySeconds: peakDay.total_seconds,
};
}

export async function GET(request: Request) {
const CONSISTENCY_DAYS = 365;
const supabase = await createClient();
const { user, profile } = await getUserWithProfile();
const { searchParams } = new URL(request.url);
Expand Down Expand Up @@ -45,21 +110,29 @@ export async function GET(request: Request) {

const now = new Date();
const sixHours = 6 * 60 * 60 * 1000;
const existingDailyStats = Array.isArray(existing?.daily_stats)
? existing.daily_stats
: [];

if (existing?.last_fetched_at) {
const lastFetch = new Date(existing.last_fetched_at).getTime();
if (now.getTime() - lastFetch < sixHours) {
if (
now.getTime() - lastFetch < sixHours &&
existingDailyStats.length >= CONSISTENCY_DAYS
) {
return NextResponse.json({ success: true, data: existing });
}
}
}

// Fetch from WakaTime API endpoints
const endDate = new Date();
endDate.setHours(0, 0, 0, 0);
const startDate = new Date();
startDate.setDate(endDate.getDate() - 6);
const endStr = endDate.toISOString().split("T")[0];
const startStr = startDate.toISOString().split("T")[0];
startDate.setHours(0, 0, 0, 0);
startDate.setDate(endDate.getDate() - (CONSISTENCY_DAYS - 1));
const endStr = formatDateYMD(endDate);
const startStr = formatDateYMD(startDate);

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

Expand Down Expand Up @@ -94,11 +167,17 @@ export async function GET(request: Request) {
range: { date: string };
grand_total: { total_seconds: number };
}) => ({
date: day.range.date,
total_seconds: day.grand_total.total_seconds,
date: toDateKey(day.range.date),
total_seconds: Math.floor(day.grand_total.total_seconds || 0),
}),
);

const snapshotMetrics = buildSnapshotMetrics(daily_stats);
const topLanguage =
Array.isArray(wakaStats.languages) && wakaStats.languages.length > 0
? wakaStats.languages[0]
: null;

if (apiKey) {
const { error } = await supabase
.from("profiles")
Expand Down Expand Up @@ -158,6 +237,35 @@ export async function GET(request: Request) {
projects: projectsResult?.projects || [],
};

const { error: snapshotError } = await supabase
.from("user_dashboard_snapshots")
.upsert(
{
user_id: user.id,
snapshot_date: endStr,
total_seconds_7d: snapshotMetrics.totalSeconds7d,
active_days_7d: snapshotMetrics.activeDays7d,
consistency_percent: snapshotMetrics.consistencyPercent,
current_streak: snapshotMetrics.currentStreak,
best_streak: snapshotMetrics.bestStreak,
peak_day: snapshotMetrics.peakDayDate,
peak_day_seconds: snapshotMetrics.peakDaySeconds,
top_language: topLanguage?.name || null,
top_language_percent:
typeof topLanguage?.percent === "number"
? Number(topLanguage.percent.toFixed(2))
: null,
updated_at: new Date().toISOString(),
},
{
onConflict: "user_id,snapshot_date",
},
);

if (snapshotError) {
console.error("Failed to upsert user dashboard snapshot", snapshotError);
}

return NextResponse.json({
success: !!statsResult && !statsError && !projectsError,
data: mergedResult,
Expand Down
Loading
Loading