diff --git a/package-lock.json b/package-lock.json index 251af664..4d5c1512 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "html-to-image": "^1.11.13", "jspdf": "^4.2.1", "jspdf-autotable": "^5.0.7", + "lucide-react": "^1.16.0", "next": "^14.2.35", "next-auth": "^4.24.7", "react": "^18", @@ -4370,6 +4371,15 @@ "node": ">=10" } }, + "node_modules/lucide-react": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.16.0.tgz", + "integrity": "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5062,7 +5072,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, diff --git a/package.json b/package.json index fd2359d8..40f98119 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "html-to-image": "^1.11.13", "jspdf": "^4.2.1", "jspdf-autotable": "^5.0.7", + "lucide-react": "^0.475.0", "next": "^14.2.35", "next-auth": "^4.24.7", "react": "^18", diff --git a/src/app/dashboard/settings/page.tsx b/src/app/dashboard/settings/page.tsx index ab68ccb5..509197bc 100644 --- a/src/app/dashboard/settings/page.tsx +++ b/src/app/dashboard/settings/page.tsx @@ -311,8 +311,8 @@ function SettingsPageContent() {
{statusMessage.message} @@ -491,7 +491,7 @@ function SettingsPageContent() {
{removeError && ( -
+
{removeError}
)} @@ -531,7 +531,7 @@ function SettingsPageContent() { onClick={() => handleRemoveAccount(account.githubId)} aria-label={`Remove ${account.githubLogin}`} disabled={removingAccountId === account.githubId} - className="rounded-lg border border-[var(--border)] px-4 py-2 text-sm font-medium text-[var(--card-foreground)] transition-colors hover:bg-red-500/10 hover:text-red-400 disabled:opacity-60" + className="rounded-lg border border-[var(--border)] px-4 py-2 text-sm font-medium text-[var(--card-foreground)] transition-colors hover:bg-[var(--error)]/10 hover:text-[var(--error)] disabled:opacity-60" > {removingAccountId === account.githubId ? "Removing..." diff --git a/src/app/globals.css b/src/app/globals.css index d1d9f210..f6f2d58d 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -13,6 +13,7 @@ --border: #cbd5e1; --accent: #6366f1; --success: #10b981; + --error: #ef4444; --accent-soft: rgba(99, 102, 241, 0.15); --accent-foreground: #ffffff; --control: #e2e8f0; @@ -32,6 +33,7 @@ --border: #334155; --accent: #818cf8; --success: #10b981; + --error: #ef4444; --accent-soft: rgba(99, 102, 241, 0.2); --accent-foreground: #ffffff; --control: #334155; diff --git a/src/components/BackToTopButton.tsx b/src/components/BackToTopButton.tsx index b71c8e87..136bc911 100644 --- a/src/components/BackToTopButton.tsx +++ b/src/components/BackToTopButton.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useState } from "react"; +import { ArrowUp } from "lucide-react"; export default function BackToTopButton() { const [isVisible, setIsVisible] = useState(false); @@ -40,21 +41,7 @@ export default function BackToTopButton() { aria-label="Scroll to top" className="fixed bottom-8 right-8 z-50 flex h-12 w-12 items-center justify-center rounded-full bg-[var(--accent)] text-[var(--accent-foreground)] shadow-lg transition-all duration-300 hover:scale-110 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-[var(--accent)] focus:ring-offset-2 focus:ring-offset-[var(--background)]" > - {/* Up Arrow SVG */} - - - +
) : error ? ( -
+

{error}

diff --git a/src/components/CommitTimeChart.tsx b/src/components/CommitTimeChart.tsx index 5076ccaa..3b2ecf00 100644 --- a/src/components/CommitTimeChart.tsx +++ b/src/components/CommitTimeChart.tsx @@ -10,6 +10,7 @@ import { Tooltip, ResponsiveContainer, } from "recharts"; +import { Sun, Cloud, Sunset, Moon } from "lucide-react"; interface TimeBlocks { morning: number; @@ -18,10 +19,15 @@ interface TimeBlocks { night: number; } +interface ChartData { + name: string; + commits: number; + icon: React.ComponentType; + key: string; +} + export default function CommitTimeChart() { - const [data, setData] = useState< - { name: string; commits: number; icon: string }[] - >([]); + const [data, setData] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [days, setDays] = useState(30); @@ -40,29 +46,29 @@ export default function CommitTimeChart() { } const blocks = res.timeBlocks; - const chartData = [ + const chartData: ChartData[] = [ { name: "Morning (6-12)", commits: blocks.morning, - icon: "Sun", + icon: Sun, key: "morning", }, { name: "Afternoon (12-18)", commits: blocks.afternoon, - icon: "CloudSun", + icon: Cloud, key: "afternoon", }, { name: "Evening (18-22)", commits: blocks.evening, - icon: "Sunset", + icon: Sunset, key: "evening", }, { name: "Night (22-6)", commits: blocks.night, - icon: "Moon", + icon: Moon, key: "night", }, ]; @@ -128,12 +134,12 @@ export default function CommitTimeChart() {
) : error ? (
-
+

{error}

diff --git a/src/components/ContributionHeatmap.tsx b/src/components/ContributionHeatmap.tsx index 8c81db44..e06b0606 100644 --- a/src/components/ContributionHeatmap.tsx +++ b/src/components/ContributionHeatmap.tsx @@ -219,8 +219,8 @@ export default function ContributionHeatmap({ {loading ? (
) : error ? ( -
-

{error} Please try refreshing.

+
+

{error} Please try refreshing.

) : ( <> diff --git a/src/components/CopyLinkButton.tsx b/src/components/CopyLinkButton.tsx index b4cd81ed..cea4c559 100644 --- a/src/components/CopyLinkButton.tsx +++ b/src/components/CopyLinkButton.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import { Link, CheckCircle } from "lucide-react"; export default function CopyLinkButton() { const [copied, setCopied] = useState(false); @@ -26,12 +27,12 @@ export default function CopyLinkButton() { > {copied ? ( <> - +
{error && ( -
+
{error}
@@ -165,7 +165,7 @@ export default function FriendComparison() { diff --git a/src/components/GoalTracker.tsx b/src/components/GoalTracker.tsx index ad8407a4..d41cc92e 100644 --- a/src/components/GoalTracker.tsx +++ b/src/components/GoalTracker.tsx @@ -1,428 +1,428 @@ -"use client"; - -import { useCallback, useEffect, useState, useRef } from "react"; - -type Recurrence = "none" | "weekly" | "monthly"; - -interface Goal { - id: string; - title: string; - target: number; - current: number; - unit: string; - recurrence: Recurrence; - period_start: string; -} - -const RECURRENCE_LABELS: Record = { - none: "One-time", - weekly: "Weekly", - monthly: "Monthly", -}; - -export default function GoalTracker() { - const [goals, setGoals] = useState([]); - const [loading, setLoading] = useState(true); - const [lastUpdated, setLastUpdated] = useState(null); - const [minutesAgo, setMinutesAgo] = useState(0); - const [title, setTitle] = useState(""); - const [target, setTarget] = useState(7); - const [unit, setUnit] = useState("commits"); - const [recurrence, setRecurrence] = useState("none"); - const [creating, setCreating] = useState(false); - const [createError, setCreateError] = useState(null); - const [confirmingId, setConfirmingId] = useState(null); - const [deletingId, setDeletingId] = useState(null); - - const [activeConfettiGoalId, setActiveConfettiGoalId] = useState(null); - const prevGoalsRef = useRef>(new Map()); - const initialLoadDoneRef = useRef(false); - - const loadGoals = useCallback(async () => { - const response = await fetch("/api/goals"); - const data: { goals: Goal[] } = await response.json(); - setGoals(data.goals ?? []); - }, []); - - useEffect(() => { - loadGoals() - .catch(() => {}) - .finally(() => { - setLoading(false); - setLastUpdated(new Date()); - setMinutesAgo(0); - }); - }, [loadGoals]); - - async function handleCreate(e: React.FormEvent) { - e.preventDefault(); - setCreating(true); - setCreateError(null); - - try { - const response = await fetch("/api/goals", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ title, target, unit, recurrence }), - }); - - if (!response.ok) { - throw new Error("Failed to create goal"); - } - } catch { - setCreateError("Failed to create goal. Please try again."); - setCreating(false); - return; - } - - setTitle(""); - setTarget(7); - setUnit("commits"); - setRecurrence("none"); - await loadGoals().catch(() => {}); - setCreating(false); - } - - async function handleDelete(id: string) { - const previousGoals = goals; - setGoals((prev) => prev.filter((g) => g.id !== id)); - setConfirmingId(null); - setDeletingId(id); - - try { - const res = await fetch(`/api/goals/${id}`, { method: "DELETE" }); - if (!res.ok) { - setGoals(previousGoals); - } - } catch { - setGoals(previousGoals); - } finally { - setDeletingId(null); - } - } - - function getCompletionLabel(goal: Goal): string { - if (goal.current >= goal.target) { - if (goal.recurrence === "weekly") return "Completed this week ✓"; - if (goal.recurrence === "monthly") return "Completed this month ✓"; - return "Completed ✓"; - } - return ""; - } - - useEffect(() => { - if (goals.length === 0) return; - - if (!initialLoadDoneRef.current) { - const map = new Map(); - for (const g of goals) { - map.set(g.id, g.current >= g.target); - } - prevGoalsRef.current = map; - initialLoadDoneRef.current = true; - return; - } - - for (const g of goals) { - const isCompleted = g.current >= g.target; - const wasCompleted = prevGoalsRef.current.get(g.id); - - if (wasCompleted === false && isCompleted) { - if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) { - setActiveConfettiGoalId(g.id); - setTimeout(() => { - setActiveConfettiGoalId((curr) => (curr === g.id ? null : curr)); - }, 2500); - } - } - - prevGoalsRef.current.set(g.id, isCompleted); - } - }, [goals]); - - useEffect(() => { - if (!lastUpdated) return; - const interval = setInterval(() => { - const diff = Math.floor((Date.now() - lastUpdated.getTime()) / 60000); - setMinutesAgo(diff); - }, 60000); - return () => clearInterval(interval); - }, [lastUpdated]); - - if (loading) { - return ( -
-
- Loading weekly goals -