diff --git a/app/api/wakatime/sync/route.ts b/app/api/wakatime/sync/route.ts index 81505dd..d47abe8 100644 --- a/app/api/wakatime/sync/route.ts +++ b/app/api/wakatime/sync/route.ts @@ -41,7 +41,14 @@ export async function GET(request: Request) { // Check last fetch const { data: existing } = await supabase .from("user_stats") - .select("*") + .select( + ` + *, + projects:user_projects ( + projects + ) + `, + ) .eq("user_id", user.id) .single(); @@ -84,14 +91,37 @@ export async function GET(request: Request) { .eq("id", user.id); } - const { data: result, error } = await supabase.from("user_stats").upsert({ - user_id: user.id, - total_seconds: Math.floor(data.data.total_seconds), - languages: data.data.languages, - operating_systems: data.data.operating_systems, - editors: data.data.editors, - last_fetched_at: new Date().toISOString(), + const { data: statsResult, error: statsError } = await supabase + .from("user_stats") + .upsert({ + user_id: user.id, + total_seconds: Math.floor(data.data.total_seconds), + languages: data.data.languages, + operating_systems: data.data.operating_systems, + editors: data.data.editors, + last_fetched_at: new Date().toISOString(), + }) + .select() + .single(); + + const { data: projectsResult, error: projectsError } = await supabase + .from("user_projects") + .upsert({ + user_id: user.id, + projects: data.data.projects, + last_fetched_at: new Date().toISOString(), + }) + .select() + .single(); + + const mergedResult = { + ...statsResult, + projects: projectsResult?.projects || [], + }; + + return NextResponse.json({ + success: !!statsResult && !statsError && !projectsError, + data: mergedResult, + error: statsError || projectsError, }); - - return NextResponse.json({ success: !!result, data: result, error }); } diff --git a/app/components/DevIcon.tsx b/app/components/DevIcon.tsx new file mode 100644 index 0000000..aee78b0 --- /dev/null +++ b/app/components/DevIcon.tsx @@ -0,0 +1,4 @@ +export const getDevIcon = (lang: string) => { + if (!lang) return null; + return ; +}; diff --git a/app/components/dashboard/Stats.tsx b/app/components/dashboard/Stats.tsx index d007a23..489d721 100644 --- a/app/components/dashboard/Stats.tsx +++ b/app/components/dashboard/Stats.tsx @@ -4,38 +4,23 @@ import { useEffect, useState } from "react"; import AOS from "aos"; import "devicon/devicon.min.css"; import { toast } from "react-toastify"; -import { - AreaChart, - Area, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ResponsiveContainer, - PieChart, - Pie, - Cell, -} from "recharts"; - -type StatsData = { +import CodingActivity from "./widgets/CodingActivity"; +import { formatHours } from "@/app/utils/time"; +import StatsCard from "./widgets/StatsCard"; +import LanguageDestribution from "./widgets/LanguageDestribution"; +import Editors from "./widgets/Editors"; +import OperatingSystem from "./widgets/OperatingSystem"; +import Projects from "./widgets/Projects"; + +export interface StatsData { total_seconds: number; daily_average: number; languages: { name: string; total_seconds: number; percent: number }[]; editors: { name: string; total_seconds: number; percent: number }[]; operating_systems: { name: string; total_seconds: number; percent: number }[]; + projects?: { name: string; total_seconds: number }[]; last_fetched_at?: string; -}; - -const CHART_COLORS = [ - "#6366f1", - "#8b5cf6", - "#a78bfa", - "#c084fc", - "#22d3ee", - "#34d399", - "#fbbf24", - "#f87171", -]; +} export default function Stats() { const [syncing, setSyncing] = useState(false); @@ -46,6 +31,7 @@ export default function Stats() { languages: [], editors: [], operating_systems: [], + projects: [], }); const fetchStats = () => { @@ -72,7 +58,7 @@ export default function Stats() { setTimeout(() => { AOS.refresh(); }, 200); - + // Trigger progress bar animation shortly after mounting/syncing const timer = setTimeout(() => { setAnimated(true); @@ -87,20 +73,6 @@ export default function Stats() { fetchStats(); }; - const 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`; - }; - - const getLanguageIcon = (lang: string) => { - if (!lang) return null; - return ; - }; - const totalHoursFormatted = formatHours(stats.total_seconds); const avgDailyFormatted = stats.daily_average ? formatHours(stats.daily_average) @@ -111,11 +83,14 @@ export default function Stats() { const days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; const avgPerDay = stats.total_seconds / 7; const dailyData = days.map((day, i) => { - const varianceRaw = 0.4 + Math.sin(i * 1.5 + 1) * 0.6; + const varianceRaw = 0.6 + Math.sin(i * 1.5 + 1) * 0.4; const variance = Math.max(0, varianceRaw); return { day, - hours: Math.max(0, parseFloat(((avgPerDay * variance) / 3600).toFixed(1))), + hours: Math.max( + 0, + parseFloat(((avgPerDay * variance) / 3600).toFixed(1)), + ), }; }); @@ -125,8 +100,14 @@ export default function Stats() { value: l.total_seconds, })); - const totalCodingProgress = Math.min(100, (stats.total_seconds / (40 * 3600)) * 100); - const dailyAverageProgress = Math.min(100, ((stats.daily_average || stats.total_seconds / 7) / (8 * 3600)) * 100); + const totalCodingProgress = Math.min( + 100, + (stats.total_seconds / (40 * 3600)) * 100, + ); + const dailyAverageProgress = Math.min( + 100, + ((stats.daily_average || stats.total_seconds / 7) / (8 * 3600)) * 100, + ); const topLangProgress = stats.languages[0]?.percent || 0; const topEditorProgress = stats.editors[0]?.percent || 0; @@ -169,10 +150,14 @@ export default function Stats() { }, ]; + /** + * i think ive seen this code before... where was it... hmmm... oh yeah, i wrote it like 5 minutes ago in the StatsCard component. maybe i should just move this logic there? nah, its fine here for now, its not like its used anywhere else and hey btw, congrations for making it this far into the code! you must be really interested in how this dashboard works. if you have any suggestions or want to contribute, feel free to reach out or check the repo on github. happy coding! Ohhh your still reading this comment? well i guess i can share a little secret with you... the key to becoming a better developer is to always keep learning and building. don't be afraid to experiment, break things, and learn from your mistakes. also, remember to take breaks and have fun with coding! it's not just about writing code, it's about creating something awesome that can make a difference. so keep pushing forward, and who knows, maybe one day you'll be the one writing comments like this in your own code! hahaha - the DevPulse Team + */ + return (
{/* Header */} -
@@ -215,280 +200,22 @@ export default function Stats() {
) : ( <> - {/* Stat Cards */} -
- {statCards.map((card, idx) => ( -
-
-

- {card.label} -

- - {card.trend} - -
-

- {card.value} -

-

{card.sub}

- {/* Mini bar */} -
{ - setAnimated(false); - setTimeout(() => setAnimated(true), 10); - }} - > -
-
-
- ))} -
+ - {/* Charts Row */}
- {/* Area Chart - Coding Activity */} -
-
-

- Coding Activity -

- Last 7 days -
-
- - - - - - - - - - - `${val}h`} - /> - [formatHours((value as number) * 3600), "Time"]} - /> - - - -
-
- - {/* Donut Chart - Language Distribution */} -
-

- Language Distribution -

-
- - - - {pieData.map((_, index) => ( - - ))} - - [formatHours(value as number), "Time"]} - /> - - -
- {/* Legend */} -
- {pieData.map((entry, idx) => ( -
-
- - {entry.name} - -
- ))} -
-
-
- - {/* Editors & OS */} -
-
-

Editors

-
- {stats.editors.slice(0, 4).map((editor, idx) => ( -
-
- - {getLanguageIcon(editor.name)} - - - {editor.name} - - {idx === 0 && ( - - PRIMARY - - )} -
- - {formatHours(editor.total_seconds)} - -
- ))} -
+
+ +
-
-

- Operating Systems -

-
- {stats.operating_systems.slice(0, 4).map((os, idx) => ( -
-
- - {getLanguageIcon(os.name)} - - - {os.name} - - {idx === 0 && ( - - MAIN - - )} -
- - {formatHours(os.total_seconds)} - -
- ))} -
+
+ + +
diff --git a/app/components/dashboard/widgets/CodingActivity.tsx b/app/components/dashboard/widgets/CodingActivity.tsx new file mode 100644 index 0000000..bb61942 --- /dev/null +++ b/app/components/dashboard/widgets/CodingActivity.tsx @@ -0,0 +1,101 @@ +import { formatHours } from "@/app/utils/time"; +import { + AreaChart, + Area, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from "recharts"; + +export default function CodingActivity({ + dailyData, +}: { + dailyData: { day: string; hours: number }[]; +}) { + return ( + <> +
+
+

Coding Activity

+ Last 7 days +
+
+ + + + + + + + + + + `${val}h`} + /> + [ + formatHours((value as number) * 3600), + "Time", + ]} + /> + + + +
+
+ + ); +} diff --git a/app/components/dashboard/widgets/Editors.tsx b/app/components/dashboard/widgets/Editors.tsx new file mode 100644 index 0000000..eacf1e4 --- /dev/null +++ b/app/components/dashboard/widgets/Editors.tsx @@ -0,0 +1,38 @@ +import { formatHours } from "@/app/utils/time"; +import { getDevIcon } from "../../DevIcon"; +import { StatsData } from "../Stats"; + +export default function Editors({ stats }: { stats: StatsData }) { + return ( + <> +
+

Editors

+
+ {stats.editors.slice(0, 4).map((editor, idx) => ( +
+
+ + {getDevIcon(editor.name)} + + + {editor.name} + + {idx === 0 && ( + + PRIMARY + + )} +
+ + {formatHours(editor.total_seconds)} + +
+ ))} +
+
+ + ) +} diff --git a/app/components/dashboard/widgets/LanguageDestribution.tsx b/app/components/dashboard/widgets/LanguageDestribution.tsx new file mode 100644 index 0000000..246b59d --- /dev/null +++ b/app/components/dashboard/widgets/LanguageDestribution.tsx @@ -0,0 +1,80 @@ +import { formatHours } from "@/app/utils/time"; +import { Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from "recharts"; + +const CHART_COLORS = [ + "#6366f1", + "#8b5cf6", + "#a78bfa", + "#c084fc", + "#22d3ee", + "#34d399", + "#fbbf24", + "#f87171", +]; + +export default function LanguageDestribution({ + pieData, +}: { + pieData: { name: string; value: number }[]; +}) { + return ( + <> +
+

+ Language Distribution +

+
+ + + + {pieData.map((_, index) => ( + + ))} + + [formatHours(value as number), "Time"]} + /> + + +
+ {/* Legend */} +
+ {pieData.map((entry, idx) => ( +
+
+ + {entry.name} + +
+ ))} +
+
+ + ); +} diff --git a/app/components/dashboard/widgets/OperatingSystem.tsx b/app/components/dashboard/widgets/OperatingSystem.tsx new file mode 100644 index 0000000..b4c069c --- /dev/null +++ b/app/components/dashboard/widgets/OperatingSystem.tsx @@ -0,0 +1,37 @@ +import { formatHours } from "@/app/utils/time"; +import { getDevIcon } from "../../DevIcon"; +import { StatsData } from "../Stats"; + +export default function OperatingSystem({ stats }: { stats: StatsData }) { + return ( + <> +
+

+ Operating Systems +

+
+ {stats.operating_systems.slice(0, 4).map((os, idx) => ( +
+
+ + {getDevIcon(os.name)} + + + {os.name} + + {idx === 0 && ( + + MAIN + + )} +
+ + {formatHours(os.total_seconds)} + +
+ ))} +
+
+ + ); +} diff --git a/app/components/dashboard/widgets/Projects.tsx b/app/components/dashboard/widgets/Projects.tsx new file mode 100644 index 0000000..c51c21f --- /dev/null +++ b/app/components/dashboard/widgets/Projects.tsx @@ -0,0 +1,45 @@ +import { formatHours } from "@/app/utils/time"; +import { StatsData } from "../Stats"; + +export default function Projects({ stats }: { stats: StatsData }) { + return ( + <> + {stats.projects ? ( + <> +
+

Projects

+
+ {stats.projects.slice(0, 4).map((project, idx) => ( +
+
+ + + {project.name} + + {idx === 0 && ( + + PRIMARY + + )} +
+ + {formatHours(project.total_seconds)} + +
+ ))} +
+
+ + ) : ( +
+
+

No project data available.

+
+
+ )} + + ); +} diff --git a/app/components/dashboard/widgets/StatsCard.tsx b/app/components/dashboard/widgets/StatsCard.tsx new file mode 100644 index 0000000..137bd11 --- /dev/null +++ b/app/components/dashboard/widgets/StatsCard.tsx @@ -0,0 +1,68 @@ +export interface StatCard { + label: string; + value: string; + sub: string; + trend: string; + trendUp: boolean; + color: string; + progress: number; // 0 to 100 +} + +export default function StatsCard({ + statCards, + animated, + setAnimated, +}: { + statCards: StatCard[]; + animated: boolean; + setAnimated: (val: boolean) => void; +}) { + return ( + <> +
+ {statCards.map((card, idx) => ( +
+
+

+ {card.label} +

+ + {card.trend} + +
+

{card.value}

+

{card.sub}

+ {/* Mini bar */} +
{ + setAnimated(false); + setTimeout(() => setAnimated(true), 10); + }} + > +
+
+
+ ))} +
+ + ); +} diff --git a/app/utils/time.ts b/app/utils/time.ts new file mode 100644 index 0000000..3547fd9 --- /dev/null +++ b/app/utils/time.ts @@ -0,0 +1,8 @@ +export const 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`; +}; diff --git a/sql/user_projects.sql b/sql/user_projects.sql new file mode 100644 index 0000000..93a53c7 --- /dev/null +++ b/sql/user_projects.sql @@ -0,0 +1,23 @@ +/* ---- User Projects Stats ----- */ +create table public.user_projects ( + user_id uuid primary key references auth.users(id) on delete cascade, + projects jsonb not null, + last_fetched_at timestamp with time zone default now() +); + +alter table public.user_projects enable row level security; + +create policy "Users can view their own projects" +on public.user_projects +for select +using (auth.uid() = user_id); + +create policy "Users can insert their own projects" +on public.user_projects +for insert +with check (auth.uid() = user_id); + +create policy "Users can update their own projects" +on public.user_projects +for update +using (auth.uid() = user_id);