+
+
+ ✨ 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 */}
+
+
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 (
+
+ );
+ }
+
+ 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}
+ {
+ setFriendUsername("");
+ setSelectedUserAvatar("");
+ }}
+ className="ml-auto text-[var(--muted-foreground)] hover:text-[var(--foreground)]"
+ title="Change user"
+ >
+ ✕
+
+
+ ) : (
+
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 && (
+
+ Clear
+
+ )}
@@ -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}
+
+
+ {/* Friend profile header */}
+
+
+ {friendData.username}
+
-
+ {/* Metrics rows */}
+
)}
@@ -430,20 +480,20 @@ function ComparisonRow({
}
return (
-
+
+
+ {label}
+
{myValue}
{suffix}
-
- {label}
-