diff --git a/src/app/u/[username]/page.tsx b/src/app/u/[username]/page.tsx index 2a653574..d57c1d07 100644 --- a/src/app/u/[username]/page.tsx +++ b/src/app/u/[username]/page.tsx @@ -10,6 +10,80 @@ import ShareProfileSection from "@/components/ShareProfileSection"; 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 { + 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"; @@ -44,6 +118,7 @@ export async function generateMetadata({ }: { params: Promise<{ username: string }>; }): Promise { + 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); @@ -139,9 +214,29 @@ export default async function PublicProfilePage({
-

- @{profile.username}'s Profile +

+ @{profile.username}'s Profile {profile.isSponsor && } + + {/* 🎯 Render Server-Calculated Time Distribution Badges Safely on Public Profile View */} + {profile.isNightOwl && ( + + + Night Owl + + )} + {profile.isEarlyBird && ( + + + Early Bird + + )}

@@ -165,6 +260,7 @@ export default async function PublicProfilePage({ )}
+
{/* Download stats card button — client component */}
@@ -225,10 +321,6 @@ export default async function PublicProfilePage({ ); } -/** - * Public variant of ContributionGraph component. - * Displays data passed as props instead of fetching it. - */ function PublicContributionGraph({ data: contributionData, }: { @@ -261,7 +353,6 @@ function PublicContributionGraph({

) : (
- {/* Simple text-based activity display for public profiles */}
{data.length} active days
@@ -288,10 +379,6 @@ function PublicContributionGraph({ ); } -/** - * Public variant of StreakTracker component. - * Displays data passed as props. - */ function PublicStreakTracker({ streak }: { streak: any }) { const stats = [ { @@ -362,10 +449,6 @@ function PublicStreakTracker({ streak }: { streak: any }) { ); } -/** - * Public variant of TopRepos component. - * Displays data passed as props. - */ function PublicTopRepos({ repos, }: { @@ -423,4 +506,4 @@ function PublicTopRepos({ )}
); -} +} \ No newline at end of file diff --git a/src/components/DashboardHeader.tsx b/src/components/DashboardHeader.tsx index 07eb688d..df4a810c 100644 --- a/src/components/DashboardHeader.tsx +++ b/src/components/DashboardHeader.tsx @@ -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 = { @@ -88,6 +90,53 @@ function useDashboardSync() { export default function DashboardHeader() { const { data: session } = useSession(); const [isPublic, setIsPublic] = useState(null); + const [greeting, setGreeting] = useState("Welcome back"); + + const [isNightOwl, setIsNightOwl] = useState(false); + const [isEarlyBird, setIsEarlyBird] = useState(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("Welcome back"); @@ -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); @@ -173,6 +221,37 @@ export default function DashboardHeader() { {/* Left Section */}
+
+
+
+ + + + + + {greeting}, {displayName}! + +
+ + {isNightOwl && ( +
+ + Night Owl +
+ )} + + {isEarlyBird && ( +
+ + Early Bird +
+ )}
{/* Dynamic Personalized Friendly Greeting Badge Element Overlay */}
@@ -192,6 +271,21 @@ export default function DashboardHeader() {

Your coding activity at a glance 🚀 +

+

+ Dashboard overview +

+

+ Dashboard +

+

+ coding activity at a glance

{minutesAgo !== null && (