diff --git a/src/App.tsx b/src/App.tsx index 7b08b20a..9d6422a1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,7 @@ import { Link, Route, Routes, useParams } from "react-router-dom"; import { AskAI } from "@/components/ask-ai"; import { ProjectListItem } from "@/components/ProjectListItem"; import { TimeSince } from "@/components/time-since"; +import { WakaTimeStats } from "@/components/wakatime-stats"; import { type ProjectEntry, projectList, projects } from "@/projects"; import { ThemeToggle } from "./components/theme-toggle"; @@ -102,7 +103,7 @@ function Home() { Age: -
+
{sortedProjects.map((project) => ( ))}
+
diff --git a/src/components/wakatime-stats.tsx b/src/components/wakatime-stats.tsx new file mode 100644 index 00000000..301bf783 --- /dev/null +++ b/src/components/wakatime-stats.tsx @@ -0,0 +1,155 @@ +import { useEffect, useMemo, useState } from "react"; + +type WakaTimeLanguage = { + name: string; + percent: number; + seconds: number; + text: string; +}; + +type WakaTimeStatsPayload = { + isCoding: boolean; + last7Text: string; + last7Seconds: number; + languages: WakaTimeLanguage[]; + updatedAt: string; +}; + +const defaultRefreshMs = 60_000; + +export function WakaTimeStats() { + const [stats, setStats] = useState(null); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + const refreshMs = useMemo(() => { + const raw = import.meta.env.VITE_WAKATIME_REFRESH_MS; + if (!raw) return defaultRefreshMs; + const parsed = Number.parseInt(raw, 10); + if (Number.isNaN(parsed) || parsed < 10_000) return defaultRefreshMs; + return parsed; + }, []); + + useEffect(() => { + let isActive = true; + + const load = async () => { + try { + const response = await fetch("/api/wakatime/stats", { + headers: { + Accept: "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`request failed (${response.status})`); + } + + const data = (await response.json()) as WakaTimeStatsPayload; + if (!isActive) return; + + setStats(data); + setError(null); + } catch (fetchError) { + if (!isActive) return; + setError( + fetchError instanceof Error + ? fetchError.message + : "Unable to fetch WakaTime stats", + ); + } finally { + if (isActive) setIsLoading(false); + } + }; + + void load(); + const timer = window.setInterval(() => { + void load(); + }, refreshMs); + + return () => { + isActive = false; + window.clearInterval(timer); + }; + }, [refreshMs]); + + return ( +
+
+

WakaTime

+

+ {stats?.updatedAt + ? `Updated ${new Date(stats.updatedAt).toLocaleTimeString()}` + : "Waiting for update"} +

+
+ + {isLoading ? ( +

Loading coding stats...

+ ) : null} + + {error ? ( +

+ WakaTime unavailable: {error} +

+ ) : null} + + {stats ? ( +
+
+

Currently coding

+

+ {stats.isCoding ? "Yes" : "No"} +

+
+ +
+

Last 7 days

+

{stats.last7Text}

+
+ +
+

Tracked languages

+

+ {stats.languages.length} +

+
+
+ ) : null} + + {stats?.languages.length ? ( +
    + {stats.languages.slice(0, 7).map((language) => ( +
  • +
    +
    + {language.name} + + {language.percent.toFixed(1)}% + +
    +
    +
    +
    +
    + + {language.text} + +
  • + ))} +
+ ) : null} +
+ ); +}