Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 99 additions & 16 deletions src/app/u/[username]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,80 @@
import ThemeToggle from "@/components/ThemeToggle";
import SponsorBadge from "@/components/SponsorBadge";
import PinnedReposWidget from "@/components/PinnedReposWidget";
import { fetchPinnedRepoDetails } from "@/lib/pinned-repos";
import { Moon, Sun } from "lucide-react"; // 🎯 UI vectors for server visibility

import {
fetchPublicTopRepos,
fetchPublicContributions,
fetchPublicStreak,
type PublicProfileData,
} from "@/lib/public-profile-data";

// Extend tracking structures to forward gamification flags seamlessly downstream
interface ExtendedPublicProfileData extends PublicProfileData {
isNightOwl: boolean;
isEarlyBird: boolean;
}

async function fetchPublicProfile(
username: string,
options: { includeAchievements?: boolean } = {}
): Promise<ExtendedPublicProfileData | null> {
const user = await getUserByUsername(username);

if (!user) return null;

const canonicalUsername = user.github_login.toLowerCase();

if (username !== canonicalUsername) {
redirect(`/u/${canonicalUsername}`);
}

const githubToken = process.env.GITHUB_TOKEN || "";

const [repos, contributions, streak, achievementsCache, spotlight] = await Promise.all([
fetchPublicTopRepos(user.github_login, githubToken, 30),
fetchPublicContributions(user.github_login, githubToken, 30),
fetchPublicStreak(user.github_login, githubToken),
options.includeAchievements
? syncGitHubAchievementsForUser({
userId: user.id,
githubLogin: user.github_login,
token: githubToken,
})
: Promise.resolve({ achievements: [], syncedAt: null, error: null }),
fetchPinnedRepoDetails(user.github_login, user.pinned_repos || [], githubToken),
]);

// Server-side parsing layout to compute hourly metrics cleanly from available repo data arrays
let nightOwlCount = 0;
let earlyBirdCount = 0;

const combinedRepos = repos || [];
combinedRepos.forEach((repo: any) => {
if (repo.last_commit_date || repo.updatedAt) {
const targetDate = repo.last_commit_date || repo.updatedAt;
const commitHour = new Date(targetDate).getHours();

if (commitHour >= 0 && commitHour <= 4) nightOwlCount++;
if (commitHour >= 5 && commitHour <= 8) earlyBirdCount++;
}
});

return {
username: user.github_login,
userId: user.id,
isSponsor: user.is_sponsor ?? false,
repos,
contributions,
streak,
achievements: achievementsCache.achievements,
achievementsError: achievementsCache.error,
spotlightRepos: spotlight,
isNightOwl: nightOwlCount >= 1,
isEarlyBird: earlyBirdCount >= 1,
};
import CopyLinkButton from "@/components/CopyLinkButton";
import { authOptions } from "@/lib/auth";
import { fetchPublicProfile } from "@/lib/public-profile-data";
Expand Down Expand Up @@ -44,6 +118,7 @@
}: {
params: Promise<{ username: string }>;
}): Promise<Metadata> {
const { username } = params;
const { username } = await params;
// Minimal lookup — avoids duplicating 3 GitHub API calls that the page already makes
const user = await getUserByUsername(username);
Expand Down Expand Up @@ -135,13 +210,33 @@
)}`;

return (
<div className="min-h-screen bg-[var(--background)] p-4 text-[var(--foreground)] transition-colors md:p-8">

Check failure on line 213 in src/app/u/[username]/page.tsx

View workflow job for this annotation

GitHub Actions / Type check

JSX element 'div' has no corresponding closing tag.
<div className="mb-8 flex flex-wrap items-start justify-between gap-4">
<div>
<div className="flex flex-wrap items-center gap-3">
<h1 className="text-3xl md:text-4xl font-bold text-[var(--foreground)] flex items-center gap-2">
@{profile.username}&apos;s Profile
<h1 className="text-3xl md:text-4xl font-bold text-[var(--foreground)] flex flex-wrap items-center gap-2">
<span>@{profile.username}&apos;s Profile</span>
{profile.isSponsor && <SponsorBadge />}

{/* 🎯 Render Server-Calculated Time Distribution Badges Safely on Public Profile View */}
{profile.isNightOwl && (
<span
title="Night Owl Milestone Badge"
className="inline-flex items-center gap-1 rounded-full bg-indigo-500/10 border border-indigo-500/30 px-2.5 py-0.5 text-xs font-bold text-indigo-400"
>
<Moon className="h-3 w-3" />
<span>Night Owl</span>
</span>
)}
{profile.isEarlyBird && (
<span
title="Early Bird Milestone Badge"
className="inline-flex items-center gap-1 rounded-full bg-amber-500/10 border border-amber-500/30 px-2.5 py-0.5 text-xs font-bold text-amber-400"
>
<Sun className="h-3 w-3" />
<span>Early Bird</span>
</span>
)}
</h1>
<CopyLinkButton url={profileUrl} />
</div>
Expand All @@ -165,6 +260,7 @@
</a>
)}
</div>
<div className="flex items-center gap-3">
{/* Download stats card button — client component */}
<div className="flex flex-wrap items-center gap-3">
<ThemeToggle />
Expand Down Expand Up @@ -223,21 +319,17 @@
</div>
</div>
);
}

Check failure on line 322 in src/app/u/[username]/page.tsx

View workflow job for this annotation

GitHub Actions / Type check

Unexpected token. Did you mean `{'}'}` or `&rbrace;`?

/**
* Public variant of ContributionGraph component.
* Displays data passed as props instead of fetching it.
*/
function PublicContributionGraph({
data: contributionData,

Check failure on line 325 in src/app/u/[username]/page.tsx

View workflow job for this annotation

GitHub Actions / Type check

'}' expected.
}: {

Check failure on line 326 in src/app/u/[username]/page.tsx

View workflow job for this annotation

GitHub Actions / Type check

Unexpected token. Did you mean `{'}'}` or `&rbrace;`?
data: {

Check failure on line 327 in src/app/u/[username]/page.tsx

View workflow job for this annotation

GitHub Actions / Type check

'}' expected.
days: number;

Check failure on line 328 in src/app/u/[username]/page.tsx

View workflow job for this annotation

GitHub Actions / Type check

'}' expected.
total: number;
data: Record<string, number>;

Check failure on line 330 in src/app/u/[username]/page.tsx

View workflow job for this annotation

GitHub Actions / Type check

Unexpected token. Did you mean `{'>'}` or `&gt;`?

Check failure on line 330 in src/app/u/[username]/page.tsx

View workflow job for this annotation

GitHub Actions / Type check

Identifier expected.
};

Check failure on line 331 in src/app/u/[username]/page.tsx

View workflow job for this annotation

GitHub Actions / Type check

Unexpected token. Did you mean `{'}'}` or `&rbrace;`?
}) {

Check failure on line 332 in src/app/u/[username]/page.tsx

View workflow job for this annotation

GitHub Actions / Type check

Unexpected token. Did you mean `{'}'}` or `&rbrace;`?
const data = Object.entries(contributionData.data ?? {})
.sort(([a], [b]) => a.localeCompare(b))
.map(([day, commits]) => ({ day, commits }));
Expand All @@ -261,7 +353,6 @@
</p>
) : (
<div className="space-y-2">
{/* Simple text-based activity display for public profiles */}
<div className="text-sm text-[var(--muted-foreground)]">
{data.length} active days
</div>
Expand All @@ -288,10 +379,6 @@
);
}

/**
* Public variant of StreakTracker component.
* Displays data passed as props.
*/
function PublicStreakTracker({ streak }: { streak: any }) {
const stats = [
{
Expand Down Expand Up @@ -362,10 +449,6 @@
);
}

/**
* Public variant of TopRepos component.
* Displays data passed as props.
*/
function PublicTopRepos({
repos,
}: {
Expand Down Expand Up @@ -423,4 +506,4 @@
)}
</div>
);
}
}
96 changes: 95 additions & 1 deletion src/components/DashboardHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import SignOutButton from "@/components/SignOutButton";
import ThemeToggle from "@/components/ThemeToggle";
import UserAvatar from "@/components/UserAvatar";
import KeyboardShortcuts from "@/components/KeyboardShortcuts";
import { Moon, Sun } from "lucide-react";

import { toast } from "sonner";

type DashboardSyncContextValue = {
Expand Down Expand Up @@ -88,6 +90,53 @@ function useDashboardSync() {
export default function DashboardHeader() {
const { data: session } = useSession();
const [isPublic, setIsPublic] = useState<boolean | null>(null);
const [greeting, setGreeting] = useState<string>("Welcome back");

const [isNightOwl, setIsNightOwl] = useState<boolean>(false);
const [isEarlyBird, setIsEarlyBird] = useState<boolean>(false);

useEffect(() => {
const computeCurrentGreeting = () => {
const currentHour = new Date().getHours();
if (currentHour >= 5 && currentHour < 12) return "Good morning ☀️";
if (currentHour >= 12 && currentHour < 17) return "Good afternoon 🌤️";
if (currentHour >= 17 && currentHour < 22) return "Good evening 🌙";
return "Burning the midnight oil 🦉";
};
setGreeting(computeCurrentGreeting());
}, []);

useEffect(() => {
if (!session?.githubLogin) return;

async function evaluateCodingDistributionMilestones() {
try {
const res = await fetch("/api/metrics/repos?days=90");
if (!res.ok) return;

const data = await res.json();
const commitsArray = data.repos || [];

let nightOwlCommitsCount = 0;
let earlyBirdCommitsCount = 0;

commitsArray.forEach((repo: any) => {
if (repo.last_commit_date) {
const commitHour = new Date(repo.last_commit_date).getHours();
if (commitHour >= 0 && commitHour <= 4) nightOwlCommitsCount++;
if (commitHour >= 5 && commitHour <= 8) earlyBirdCommitsCount++;
}
});

if (nightOwlCommitsCount >= 1) setIsNightOwl(true);
if (earlyBirdCommitsCount >= 1) setIsEarlyBird(true);
} catch (err) {
console.error("Failed to compile milestone hour distribution profiles:", err);
}
}

evaluateCodingDistributionMilestones();
}, [session]);
const [copied, setCopied] = useState(false);
const [greeting, setGreeting] = useState<string>("Welcome back");

Expand Down Expand Up @@ -133,7 +182,6 @@ export default function DashboardHeader() {
async function loadSettings() {
try {
const res = await fetch("/api/user/settings");

if (res.ok) {
const data = await res.json();
setIsPublic(data.is_public === true);
Expand Down Expand Up @@ -173,6 +221,37 @@ export default function DashboardHeader() {

{/* Left Section */}
<div>
<div className="flex flex-col gap-1.5">
<div className="flex flex-wrap items-center gap-2">
<div className="inline-flex items-center gap-1.5 rounded-full bg-[var(--accent)]/10 border border-[var(--accent)]/20 px-2.5 py-0.5 text-xs font-semibold text-[var(--accent)] transition-all duration-300">
<span className="relative flex h-1.5 w-1.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[var(--accent)] opacity-75"></span>
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-[var(--accent)]"></span>
</span>
<span>
{greeting}, {displayName}!
</span>
</div>

{isNightOwl && (
<div
title="Night Owl Milestone: You push code between Midnight and 4 AM!"
className="inline-flex items-center gap-1 rounded-full bg-indigo-500/10 border border-indigo-500/30 px-2 py-0.5 text-[11px] font-bold text-indigo-400 transition-all duration-300 hover:bg-indigo-500/20 cursor-help"
>
<Moon className="h-3 w-3 shrink-0 text-indigo-400" />
<span>Night Owl</span>
</div>
)}

{isEarlyBird && (
<div
title="Early Bird Milestone: You push code between 5 AM and 8 AM!"
className="inline-flex items-center gap-1 rounded-full bg-amber-500/10 border border-amber-500/30 px-2 py-0.5 text-[11px] font-bold text-amber-400 transition-all duration-300 hover:bg-amber-500/20 cursor-help"
>
<Sun className="h-3 w-3 shrink-0 text-amber-400" />
<span>Early Bird</span>
</div>
)}
<div className="flex flex-col gap-1">
{/* Dynamic Personalized Friendly Greeting Badge Element Overlay */}
<div className="inline-flex items-center gap-1.5 self-start rounded-full bg-[var(--accent)]/10 border border-[var(--accent)]/20 px-2.5 py-0.5 text-xs font-semibold text-[var(--accent)] transition-all duration-300">
Expand All @@ -192,6 +271,21 @@ export default function DashboardHeader() {

<p className="mt-2 text-sm md:text-base text-[var(--muted-foreground)]">
Your coding activity at a glance 🚀
<div className="min-w-0">
<p
className="text-[11px] font-semibold uppercase tracking-[0.24em] text-[var(--muted-foreground)]"
style={{ fontFamily: "var(--font-jetbrains, ui-monospace, monospace)" }}
>
Dashboard overview
</p>
<h1 className="mt-2 bg-gradient-to-r from-[var(--foreground)] via-[var(--foreground)] to-[var(--accent)] bg-clip-text text-3xl font-extrabold text-transparent md:text-4xl">
Dashboard
</h1>
<p
className="mt-2 max-w-xl text-sm leading-6 text-[var(--muted-foreground)]"
style={{ fontFamily: "var(--font-jetbrains, ui-monospace, monospace)", letterSpacing: "0.06em" }}
>
coding activity at a glance
</p>
{minutesAgo !== null && (
<p className="mt-1 text-xs text-[var(--muted-foreground)]">
Expand Down
Loading