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
145 changes: 75 additions & 70 deletions src/app/u/[username]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,45 @@
import { Metadata } from "next";
import BadgeSection from "@/components/BadgeSection";
import StatsCard from "@/components/StatsCard";
import CopyLinkButton from "@/components/CopyLinkButton";
import { getUserByUsername } from "@/lib/supabase";
import {
fetchPublicTopRepos,
fetchPublicContributions,
fetchPublicStreak,
type PublicProfileData,
} from "@/lib/public-profile-data";
import BackToDashboard from "@/components/BackToDashboard";
interface PublicProfileData {
username: string;
userId: string;
repos: Array<{ name: string; commits: number; url: string }>;
contributions: {
days: number;
total: number;
data: Record<string, number>;
};
streak: {
current: number;
longest: number;
lastCommitDate: string | null;
totalActiveDays: number;
};
}

async function fetchPublicProfile(
username: string
username: string,
): Promise<PublicProfileData | null> {
const user = await getUserByUsername(username);
if (!user) return null;
const baseUrl =
process.env.NEXT_PUBLIC_APP_URL ||
process.env.NEXTAUTH_URL ||
"http://localhost:3000";

const githubToken = process.env.GITHUB_TOKEN;
const [repos, contributions, streak] = await Promise.all([
fetchPublicTopRepos(user.github_login, githubToken, 30),
fetchPublicContributions(user.github_login, githubToken, 30),
fetchPublicStreak(user.github_login, githubToken),
]);
try {
const res = await fetch(`${baseUrl}/api/public/${username}`, {
cache: "no-store",
});

return {
username: user.github_login,
userId: user.id,
repos,
contributions,
streak,
};
if (!res.ok) {
return null;
}

return res.json();
} catch (error) {
console.error(`Error fetching public profile for ${username}:`, error);
return null;
}
}

export async function generateMetadata({
Expand All @@ -40,7 +50,10 @@ export async function generateMetadata({
const { username } = params;
const profile = await fetchPublicProfile(username);

const baseUrl = process.env.NEXT_PUBLIC_APP_URL || process.env.NEXTAUTH_URL || "http://localhost:3000";
const baseUrl =
process.env.NEXT_PUBLIC_APP_URL ||
process.env.NEXTAUTH_URL ||
"http://localhost:3000";
const profileUrl = `${baseUrl}/u/${username}`;

if (!profile) {
Expand Down Expand Up @@ -79,22 +92,12 @@ export default async function PublicProfilePage({
if (!profile) {
return (
<div className="min-h-screen bg-[var(--background)] p-4 md:p-8 text-[var(--foreground)] transition-colors flex items-center justify-center">
<div className="text-center max-w-md">
<div className="text-center">
<h1 className="text-3xl md:text-4xl font-bold mb-2">
Profile Not Found
</h1>
<p className="text-[var(--muted-foreground)] mb-2">
This profile is not available or has not been made public.
</p>
<p className="text-sm text-[var(--muted-foreground)] mb-6">
If this is your profile, go to{" "}
<a
href="/dashboard/settings"
className="text-[var(--accent)] underline hover:opacity-80"
>
Settings
</a>{" "}
and enable <strong>Public Profile</strong>.
<p className="text-[var(--muted-foreground)] mb-6">
This profile is not available or is private.
</p>
<a
href="/"
Expand All @@ -107,33 +110,23 @@ export default async function PublicProfilePage({
);
}

const avatarUrl = `https://avatars.githubusercontent.com/${profile.username}`;
const topRepo = profile.repos[0]?.name ?? "";

return (
<div className="min-h-screen bg-[var(--background)] p-4 md:p-8 text-[var(--foreground)] transition-colors">
<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)]">
@{profile.username}&apos;s Profile
</h1>
<CopyLinkButton />
</div>
<p className="mt-2 text-[var(--muted-foreground)]">
GitHub activity and coding stats
</p>
</div>
{/* Download stats card button — client component */}
<StatsCard
username={profile.username}
avatarUrl={avatarUrl}
currentStreak={profile.streak.current}
longestStreak={profile.streak.longest}
totalCommits={profile.contributions.total}
topRepo={topRepo}
/>
</div>
{/* Header */}

<div className="mb-8 flex flex-col gap-4">
<BackToDashboard username={profile.username} />

<div>
<h1 className="text-3xl md:text-4xl font-bold text-[var(--foreground)]">
@{profile.username}&apos;s Profile
</h1>

<p className="mt-2 text-[var(--muted-foreground)]">
GitHub activity and coding stats
</p>
</div>
</div>

{/* Row 1: Contribution graph + Streak */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
Expand Down Expand Up @@ -205,11 +198,9 @@ function PublicContributionGraph({
className="aspect-square rounded-sm"
style={{
backgroundColor:
day.commits > 0 ? "var(--accent)" : "var(--control)",
opacity:
day.commits > 0
? Math.max(0.2, Math.min(day.commits / 10, 1))
: 1,
? `hsl(var(--accent) / ${Math.min(day.commits / 10, 1)})`
: "var(--control)",
}}
title={`${day.day}: ${day.commits} commits`}
/>
Expand All @@ -225,7 +216,16 @@ function PublicContributionGraph({
* Public variant of StreakTracker component.
* Displays data passed as props.
*/
function PublicStreakTracker({ streak }: { streak: any }) {
function PublicStreakTracker({
streak,
}: {
streak: {
current: number;
longest: number;
lastCommitDate: string | null;
totalActiveDays: number;
};
}) {
const stats = [
{
label: "Current Streak",
Expand Down Expand Up @@ -321,7 +321,7 @@ function PublicTopRepos({
{repos.map((repo, idx) => {
const barWidth = Math.max(
Math.round((repo.commits / maxCommits) * 100),
4
4,
);
const shortName = repo.name.split("/")[1] ?? repo.name;
return (
Expand Down Expand Up @@ -357,3 +357,8 @@ function PublicTopRepos({
</div>
);
}





26 changes: 26 additions & 0 deletions src/components/BackToDashboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"use client";

import Link from "next/link";
import { useSession } from "next-auth/react";

interface Props {
username: string;
}

export default function BackToDashboard({ username }: Props) {
const { data: session } = useSession();

const currentUser = session?.githubLogin;
const isOwner = currentUser === username;

if (!isOwner) return null;

return (
<Link
href="/dashboard"
className="inline-block mb-4 text-[var(--muted-foreground)] hover:text-[var(--card-foreground)] transition-colors"
>
← Back to dashboard
</Link>
);
}
Loading