diff --git a/next.config.mjs b/next.config.mjs index 3c5461edb..8c1b8f370 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -145,6 +145,10 @@ const nextConfig = { protocol: "https", hostname: "github.githubassets.com", }, + { + protocol: "https", + hostname: "via.placeholder.com", + }, ], }, async headers() { diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index e30f799d4..1f3af4fc8 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,12 +1,87 @@ +import LazyWidget from "@/components/LazyWidget"; +import DiscussionsWidget from "@/components/DiscussionsWidget"; +import CommunityMetrics from "@/components/CommunityMetrics"; +import GoalTracker from "@/components/GoalTracker"; import TodayFocusHero from "@/components/TodayFocusHero"; import DashboardHeader from "@/components/DashboardHeader"; import ExportButton from "@/components/ExportButton"; import Link from "next/link"; +import PersonalRecords from "@/components/PersonalRecords"; +import LocalCodingTime from "@/components/LocalCodingTime"; +import CodingTimeWidget from "@/components/CodingTimeWidget"; +import RecentActivity from "@/components/RecentActivity"; +import FriendComparison from "@/components/FriendComparison"; import { ChevronRight } from "lucide-react"; import { authOptions } from "@/lib/auth"; import { getServerSession } from "next-auth"; import { redirect } from "next/navigation"; import DashboardSSEProvider from "@/components/DashboardSSEProvider"; + +const SkeletonCard = () => ( +
+
+
+
+); + +const ContributionGraphSkeleton = () => ( +
+

Your Commits

+
+
+); + +const PRMetricsSkeleton = () => ( +
+

PR Analytics

+
+
+); + +const CodingActivityInsightsCard = dynamic( + () => import("@/components/CodingActivityInsightsCard"), + { ssr: false, loading: () => }, +); + +const ActivityRingChart = dynamic( + () => import("@/components/ActivityRingChart"), + { ssr: false, loading: () => }, +); + +const ContributionGraph = dynamic( + () => import("@/components/ContributionGraph"), + { ssr: false, loading: () => }, +); + +const ContributionHeatmap = dynamic( + () => import("@/components/ContributionHeatmap"), + { ssr: false, loading: () => }, +); + +const PRMetrics = dynamic(() => import("@/components/PRMetrics"), { + ssr: false, + loading: () => , +}); + +const PRBreakdownChart = dynamic( + () => import("@/components/PRBreakdownChart"), + { ssr: false, loading: () => }, +); + +const CommitTimeChart = dynamic( + () => import("@/components/CommitTimeChart"), + { ssr: false, loading: () => }, +); + +const PRReviewTrendChart = dynamic( + () => import("@/components/PRReviewTrendChart"), + { ssr: false, loading: () => }, +); import StreakAtRiskBanner from "@/components/StreakAtRiskBanner"; import ThrottleBanner from "@/components/ThrottleBanner"; import CustomizableDashboard from "@/components/dashboard/CustomizableDashboard"; @@ -22,25 +97,29 @@ export default async function DashboardPage() { {/* Quick actions */} -
- {/* Left side actions */} -
- - Year in Code - - - - Settings - -
- -
+
+ + ✨ Year in Code + + + + + + Compare Friends + + + Settings + +
@@ -73,12 +152,100 @@ export default async function DashboardPage() { Generate an ATS-Friendly CV Backed by Your Real Code -

- Analyze your GitHub contributions, merged PRs, and lines of code - changed to automatically generate professional bullet points for - your target roles. -

+ {/* Right: streak + coding time */} +
+ + + +
+ + {/* Repo analytics explorer — full width */} +
+ }> + + +
+ + {/* -- Row 2: PR metrics + Community metrics -- */} +
+ + +
+ + {/* PR breakdown + commit time — 2-col so charts have room */} +
+ }> + + + }> + + +
+ + {/* Activity ring — full width */} +
+ }> + + +
+ + {/* Coding activity insights — full width */} +
+ }> + + +
+ + {/* PR review trend — full width */} +
+ }> + + +
+ + {/* -- Row 3: Issues (2/3) + CI analytics (1/3) -- */} +
+
+ }> + + +
+ +
+
+ + }> + + + }> + + + }> + + +
+
+ + }> + + + }> + + + }> + +
+
+
+ + + {/* 4. GOALS & INSIGHTS */} +
+
+
+

Goals & Insights

+
import("@/components/ContributionGraph"), + { ssr: false } +); + +export default function FriendComparePage() { + const { data: session, status } = useSession(); + const [showCommitActivity, setShowCommitActivity] = useState(false); + const [compareUsername, setCompareUsername] = useState(null); + + useEffect(() => { + if (status === "unauthenticated") { + redirect("/"); + } + }, [status]); + + useEffect(() => { + const handleShowCommitActivity = (e: Event) => { + const customEvent = e as CustomEvent<{ username?: string }>; + const username = customEvent.detail?.username; + setCompareUsername(username || null); + setShowCommitActivity(true); + }; + + const handleClearCommitActivity = () => { + setShowCommitActivity(false); + setCompareUsername(null); + }; + + window.addEventListener("devtrack:show-commit-activity", handleShowCommitActivity as EventListener); + window.addEventListener("devtrack:clear-compare-user", handleClearCommitActivity); + return () => { + window.removeEventListener("devtrack:show-commit-activity", handleShowCommitActivity as EventListener); + window.removeEventListener("devtrack:clear-compare-user", handleClearCommitActivity); + }; + }, []); + + // When showCommitActivity becomes true, dispatch the compare event after a tick + useEffect(() => { + if (showCommitActivity && compareUsername) { + // Dispatch after the component has fully mounted (1000ms delay ensures dynamic import + listener setup) + const timer = setTimeout(() => { + window.dispatchEvent( + new CustomEvent("devtrack:compare-user", { + detail: { username: compareUsername }, + }) + ); + // Scroll to the element + const element = document.getElementById("contribution-activity"); + if (element) { + const elementPosition = element.getBoundingClientRect().top; + const offsetPosition = elementPosition + window.pageYOffset - 100; + window.scrollTo({ top: offsetPosition, behavior: "smooth" }); + } + }, 1000); + return () => clearTimeout(timer); + } + }, [showCommitActivity, compareUsername]); + + // Auto-show commit activity if a friend was persisted on page refresh + useEffect(() => { + if (typeof window !== "undefined") { + try { + const persistedFriend = localStorage.getItem("devtrack:compare_username"); + if (persistedFriend) { + setCompareUsername(persistedFriend); + setShowCommitActivity(true); + } + } catch { + // Silently fail if localStorage is not available + } + } + }, []); + + if (status === "loading") { + return ( +
+
+
+

Loading...

+
+
+ ); + } + + return ( +
+ {/* Header */} +
+ + + + + Dashboard + +

+ Friend Comparison +

+

+ Compare your GitHub stats with friends and see how you stack up +

+
+ + {/* Main content */} +
+ + + {/* Commit Activity Comparison - Only rendered when button is clicked */} + {showCommitActivity && ( + + )} +
+
+ ); +} diff --git a/src/components/FriendComparison.tsx b/src/components/FriendComparison.tsx index 74904ce64..d3f5ec583 100644 --- a/src/components/FriendComparison.tsx +++ b/src/components/FriendComparison.tsx @@ -34,6 +34,7 @@ function FriendComparison() { if (typeof window === "undefined") return ""; return localStorage.getItem(STORAGE_KEY) ?? ""; }); + const [selectedUserAvatar, setSelectedUserAvatar] = useState(""); const [comparingUser, setComparingUser] = useState(""); const [myData, setMyData] = useState(null); const [friendData, setFriendData] = useState(null); @@ -132,6 +133,7 @@ function FriendComparison() { const chooseSuggestion = (user: SuggestedUser) => { setFriendUsername(user.username); + setSelectedUserAvatar(user.avatarUrl); setSuppressNextSuggestFetch(true); setSuggestions([]); setSuggestOpen(false); @@ -163,6 +165,11 @@ function FriendComparison() { detail: { username: trimmed }, }) ); + window.dispatchEvent( + new CustomEvent("devtrack:show-commit-activity", { + detail: { username: trimmed }, + }) + ); } } catch (e) { setError("An error occurred"); @@ -178,6 +185,7 @@ function FriendComparison() { const clearComparison = () => { setFriendUsername(""); + setSelectedUserAvatar(""); setComparingUser(""); setFriendData(null); setError(""); @@ -190,12 +198,6 @@ function FriendComparison() { const handleCommitActivityClick = (e: React.MouseEvent) => { e.preventDefault(); - const element = document.getElementById("contribution-activity"); - if (element) { - const elementPosition = element.getBoundingClientRect().top; - const offsetPosition = elementPosition + window.pageYOffset - 100; - window.scrollTo({ top: offsetPosition, behavior: "smooth" }); - } }; return ( @@ -216,39 +218,63 @@ function FriendComparison() { className="flex flex-col sm:flex-row gap-2 w-full" >
- setFriendUsername(e.target.value)} - onFocus={() => { - if (suggestions.length > 0) setSuggestOpen(true); - }} - onKeyDown={(e) => { - if (!suggestOpen || suggestions.length === 0) return; - - if (e.key === "ArrowDown") { - e.preventDefault(); - setActiveIndex((prev) => Math.min(prev + 1, suggestions.length - 1)); - } else if (e.key === "ArrowUp") { - e.preventDefault(); - setActiveIndex((prev) => Math.max(prev - 1, 0)); - } else if (e.key === "Enter") { - if (activeIndex >= 0 && activeIndex < suggestions.length) { + {selectedUserAvatar && friendUsername ? ( +
+ {`${friendUsername} + {friendUsername} + +
+ ) : ( + setFriendUsername(e.target.value)} + onFocus={() => { + if (suggestions.length > 0) setSuggestOpen(true); + }} + onKeyDown={(e) => { + if (!suggestOpen || suggestions.length === 0) return; + + if (e.key === "ArrowDown") { e.preventDefault(); - chooseSuggestion(suggestions[activeIndex]); + setActiveIndex((prev) => Math.min(prev + 1, suggestions.length - 1)); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + setActiveIndex((prev) => Math.max(prev - 1, 0)); + } else if (e.key === "Enter") { + if (activeIndex >= 0 && activeIndex < suggestions.length) { + e.preventDefault(); + chooseSuggestion(suggestions[activeIndex]); + } + } else if (e.key === "Escape") { + setSuggestOpen(false); + setActiveIndex(-1); } - } else if (e.key === "Escape") { - setSuggestOpen(false); - setActiveIndex(-1); - } - }} - aria-autocomplete="list" - aria-expanded={suggestOpen} - aria-controls="friend-compare-suggestions" - className="w-full rounded-md border border-[var(--border)] bg-[var(--background)] px-3 py-2 text-sm focus-visible:border-[var(--accent)]" - /> + }} + aria-autocomplete="list" + aria-expanded={suggestOpen} + aria-controls="friend-compare-suggestions" + className="w-full rounded-md border border-[var(--border)] bg-[var(--background)] px-3 py-2 text-sm outline-none focus:border-[var(--accent)]" + /> + )} {suggestOpen && suggestions.length > 0 && (
{loading ? "Loading..." : "Compare"} + + {friendData && ( + + )}
@@ -337,14 +373,41 @@ function FriendComparison() { )}
-
-
-
You ({myData.username})
-
Metric
-
Them ({friendData.username})
+
+ {/* Header with profile info */} +
+ {/* Metric column header */} +
+ Metric +
+ + {/* My profile header */} +
+ {myData.username} + {myData.username} +
+ + {/* Friend profile header */} +
+ {friendData.username} + {friendData.username} +
-
+ {/* Metrics rows */} +
+
+ +
)} -
- - View Commit Activity - - -
)} @@ -430,20 +480,20 @@ function ComparisonRow({ } return ( -
+
+
+ {label} +
{myValue} {suffix}
-
- {label} -