Skip to content
Closed
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
14 changes: 7 additions & 7 deletions app/(public)/leaderboard/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { createClient } from "../../../lib/supabase/server";
import LeaderboardTable, { NonNullableMember } from "../../../components/LeaderboardTable";
import LeaderboardHeader from "@/app/components/leaderboard/Header";
import BackButton from "@/app/components/leaderboard/BackButton";
import Footer from "@/app/components/layout/Footer";
import CTA from "@/app/components/layout/CTA";

Expand Down Expand Up @@ -52,13 +51,14 @@ export default async function LeaderboardPage(props: {

return (
<div className="min-h-screen bg-[#0a0a1a] text-white grid-bg relative">
<div className="max-w-5xl mx-auto p-6 md:p-10 relative z-10">
<BackButton />
<div className="w-full max-w-[1600px] mx-auto p-0 sm:p-6 md:p-10 relative z-10">
<LeaderboardHeader leaderboard={leaderboard} isOwner={isOwner} />
<LeaderboardTable
members={members as NonNullableMember[]}
ownerId={user?.id}
/>
<div className="px-4 sm:px-0">
<LeaderboardTable
members={members as NonNullableMember[]}
ownerId={user?.id}
/>
</div>
</div>

{!user && <CTA />}
Expand Down
2 changes: 1 addition & 1 deletion app/(public)/leaderboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export default async function Leaderboards() {
return (
<div className="min-h-screen bg-[#0a0a1a] text-white grid-bg relative">
<div className="max-w-5xl mx-auto p-6 md:p-10 relative z-10">
<BackButton />
<BackButton href="/dashboard/leaderboards" />

<div className="flex justify-center items-center gap-3 mb-8">
<Image src="/logo.svg" alt="DevPulse Logo" width={36} height={36} />
Expand Down
1 change: 0 additions & 1 deletion app/components/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPaperPlane, faPlus } from "@fortawesome/free-solid-svg-icons";
import Conversations from "./chat/Conversations";
import Messages from "./chat/Messages";
import { toast } from "react-toastify";

export interface Conversation {
id: string;
Expand Down
341 changes: 197 additions & 144 deletions app/components/LeaderboardTable.tsx

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions app/components/chat/Conversations.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"use client";
import { User } from "@supabase/supabase-js";
import { Conversation } from "../Chat";

Expand Down Expand Up @@ -32,3 +33,4 @@ export default function Conversations({
</>
);
}

8 changes: 5 additions & 3 deletions app/components/chat/Messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,15 @@ export default function Messages({

return (
<>
{/* @ts-expect-error atomDark style type not compatible with SyntaxHighlighter */}
{ }

Comment on lines 55 to +57
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The standalone {} JSX expression renders an object, which will throw at runtime (“Objects are not valid as a React child”). Remove this expression entirely (it doesn’t appear to be doing anything).

Copilot uses AI. Check for mistakes.
<SyntaxHighlighter
style={atomDark as any}

style={atomDark as { [key: string]: React.CSSProperties }}
language={match ? match[1] : "text"}
PreTag="pre"
className="rounded-md text-sm"
{...props}
{...(props as Record<string, unknown>)}
>
{String(children).replace(/\n$/, "")}
</SyntaxHighlighter>
Expand Down
1 change: 0 additions & 1 deletion app/components/dashboard/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
faTrophy,
faChevronLeft,
faChevronRight,
faGear,
faMessage,
} from "@fortawesome/free-solid-svg-icons";
import type { IconDefinition } from "@fortawesome/free-solid-svg-icons";
Expand Down
2 changes: 2 additions & 0 deletions app/components/dashboard/widgets/StatsCard.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"use client";
export interface StatCard {
label: string;
value: string;
Expand Down Expand Up @@ -72,3 +73,4 @@ export default function StatsCard({
</div>
);
}

13 changes: 1 addition & 12 deletions app/components/landing-page/TopLeaderbord.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,7 @@
export interface Category {
name: string;
text: string;
hours: number;
decimal: string;
digital: string;
minutes: number;
percent: number;
total_seconds: number;
}

export interface TopMember {
email: string;
total_seconds: number;
categories?: Category[];
categories?: { name: string; total_seconds: number }[];
}

export default function TopLeaderboard({
Expand Down
4 changes: 2 additions & 2 deletions app/components/leaderboard/BackButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { useRouter } from "next/navigation";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowLeft } from "@fortawesome/free-solid-svg-icons";

export default function BackButton() {
export default function BackButton({ href = "/dashboard/leaderboards" }: { href?: string }) {
const router = useRouter();

return (
<button
onClick={() => router.push("/dashboard")}
onClick={() => router.push(href)}
className="flex items-center gap-2 text-sm font-medium text-gray-400 hover:text-indigo-400 transition-colors mb-6 group w-fit"
>
<div className="w-8 h-8 rounded-full bg-white/5 border border-white/10 flex items-center justify-center group-hover:bg-indigo-500/10 group-hover:border-indigo-500/30 transition-all">
Expand Down
41 changes: 41 additions & 0 deletions app/components/leaderboard/Banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, { useMemo } from "react";
import Image from "next/image";

export default function Banner({ name, imageUrl }: { name: string, imageUrl?: string }) {
const gradient = useMemo(() => {
let hash = 0;
Comment on lines +1 to +6
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This component uses useMemo, but it’s missing a "use client" directive. In the Next.js App Router this will fail to compile as a Server Component. Add "use client" at the top (or remove the hook and compute the gradient without React hooks).

Copilot uses AI. Check for mistakes.
for (let i = 0; i < name.length; i++) {
hash = name.charCodeAt(i) + ((hash << 5) - hash);
}
const c1 = `hsl(${Math.abs(hash) % 360}, 70%, 40%)`;
const c2 = `hsl(${(Math.abs(hash) + 60) % 360}, 80%, 30%)`;
const c3 = `hsl(${(Math.abs(hash) + 120) % 360}, 60%, 20%)`;

return `linear-gradient(135deg, ${c1}, ${c2}, ${c3})`;
}, [name]);

return (
<div className="w-full relative sm:rounded-2xl md:rounded-3xl overflow-hidden h-40 sm:h-56 md:h-72 shadow-2xl border-t border-b sm:border border-white/5 bg-[#121226]">
{imageUrl ? (
<Image
src={imageUrl}
alt={`${name} banner`}
fill
className="object-cover opacity-80"
priority
/>
) : (
<div
className="absolute inset-0 opacity-80"
style={{ background: gradient }}
/>
)}
{/* Decorative patterns */}
<div className="absolute inset-0 mix-blend-overlay opacity-30" style={{
backgroundImage: "radial-gradient(circle at 2px 2px, rgba(255,255,255,0.2) 1px, transparent 0)",
backgroundSize: "32px 32px"
}} />
<div className="absolute inset-x-0 bottom-0 h-1/2 bg-gradient-to-t from-[#0a0a1a] to-transparent opacity-80" />
</div>
);
}
64 changes: 45 additions & 19 deletions app/components/leaderboard/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { toast } from "react-toastify";
import { useRouter } from "next/navigation";
import Image from "next/image";
import { Database } from "@/app/supabase-types";
import Banner from "./Banner";
import BackButton from "./BackButton";
import InviteFriendsButton from "./InviteFriendsButton";

type LeaderboardRow = Database["public"]["Tables"]["leaderboards"]["Row"];

Expand Down Expand Up @@ -69,26 +72,49 @@ export default function LeaderboardHeader({

return (
<>
<div className="group relative mb-8 text-center">
<div className="flex justify-center items-center gap-3">
<Image src="/logo.svg" alt="DevPulse Logo" width={36} height={36} />
<h1 className="text-3xl font-bold text-white">{leaderboard.name}</h1>

{isOwner && (
<button
onClick={() => setOpen(true)}
className="opacity-0 group-hover:opacity-100 transition text-gray-600 hover:text-indigo-400 p-1.5"
>
<FontAwesomeIcon icon={faPencil} className="text-sm" />
</button>
)}
<div className="group relative mb-20 sm:mb-24">
{/* Using a temporary placeholder banner image */}
<Banner name={leaderboard.name} imageUrl="https://images.unsplash.com/photo-1550751827-4bd374c3f58b?q=80&w=2070&auto=format&fit=crop" />
Comment on lines +76 to +77
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hard-coded Unsplash URL is labeled as a “temporary placeholder”. If this isn’t meant to ship, prefer omitting imageUrl so the gradient fallback is used, or source the banner image from persisted leaderboard data/config instead of a baked-in external URL.

Suggested change
{/* Using a temporary placeholder banner image */}
<Banner name={leaderboard.name} imageUrl="https://images.unsplash.com/photo-1550751827-4bd374c3f58b?q=80&w=2070&auto=format&fit=crop" />
{/* Using the default banner background (gradient fallback) */}
<Banner name={leaderboard.name} />

Copilot uses AI. Check for mistakes.

Comment on lines +75 to +78
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR title/description say “update navigation”, but this change set also adds new leaderboard UI components, animations, and dependencies. Please update the PR description to reflect the full scope (or split into smaller PRs) so reviewers can assess impact accurately.

Copilot uses AI. Check for mistakes.
{/* Top actions overlay */}
<div className="absolute top-4 left-4 right-4 flex items-start justify-between z-20 pointer-events-none">
<div className="pointer-events-auto">
<BackButton />
</div>
</div>

<div className="absolute left-6 right-4 sm:left-8 sm:right-8 -bottom-14 sm:-bottom-16 flex items-end justify-between gap-3 sm:gap-6 z-10">
<div className="flex items-end gap-3 sm:gap-6 flex-1 min-w-0">
<div className="w-20 h-20 sm:w-28 sm:h-28 rounded-2xl bg-[#0a0a1a] p-1.5 sm:p-2 shadow-2xl shrink-0">
<div className="w-full h-full rounded-xl bg-[#121226] border border-white/5 flex items-center justify-center overflow-hidden relative">
<Image src="/logo.svg" alt="DevPulse Logo" width={40} height={40} className="object-contain opacity-80 sm:w-[50px] sm:h-[50px]" />
</div>
</div>

<div className="mb-2 sm:mb-3 max-w-[calc(100%-120px)] sm:max-w-xl">
<h1 className="text-2xl sm:text-4xl font-extrabold text-white tracking-tight flex items-center gap-3 truncate">
{leaderboard.name}
{isOwner && (
<button
onClick={() => setOpen(true)}
className="opacity-0 group-hover:opacity-100 transition text-gray-400 hover:text-indigo-400 p-1 shrink-0"
>
<FontAwesomeIcon icon={faPencil} className="text-sm sm:text-[16px]" />
</button>
)}
</h1>
<p className="text-gray-400 mt-1 text-sm sm:text-base font-medium truncate sm:whitespace-normal leading-relaxed">
{leaderboard.description && leaderboard.description?.length > 0
? leaderboard.description
: `Join ${leaderboard.name} to track your coding metrics, compete with fellow developers, and showcase your engineering skills.`}
</p>
</div>
</div>

<div className="mb-2 sm:mb-3 shrink-0 scale-90 sm:scale-95 origin-bottom-right">
<InviteFriendsButton joinCode={leaderboard?.join_code} leaderboardName={leaderboard.name} />
</div>
</div>

<p className="text-gray-500 mt-2 text-sm">
{leaderboard.description && leaderboard.description?.length > 0
? leaderboard.description
: "No description available."}
</p>
</div>

{open && (
Expand Down
32 changes: 32 additions & 0 deletions app/components/leaderboard/InviteFriendsButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"use client";

import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faShareNodes } from "@fortawesome/free-solid-svg-icons";
import { toast } from "react-toastify";

export default function InviteFriendsButton({ joinCode, leaderboardName }: { joinCode?: string; leaderboardName?: string }) {
const handleInvite = () => {
if (typeof window !== "undefined") {
const inviteUrl = joinCode
? `${window.location.origin}/join/${joinCode}`
: window.location.href; // fallback

const message = leaderboardName
? `Join my coding leaderboard "${leaderboardName}" on DevPulse!\n\nTrack metrics, compete with fellow developers, and showcase your engineering skills.\n\nJoin here: ${inviteUrl}`
: `Join my coding leaderboard on DevPulse!\n\nJoin here: ${inviteUrl}`;

navigator.clipboard.writeText(message);
toast.success("Invite message copied to clipboard!");
}
Comment on lines +7 to +20
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

navigator.clipboard.writeText(...) returns a promise and can fail (non-secure context, permission denied, unsupported). Await it and handle errors (show a toast error), and guard for navigator.clipboard being unavailable. Also consider setting type="button" to avoid accidental form submission if this button is ever placed inside a form.

Copilot uses AI. Check for mistakes.
};

return (
<button
onClick={handleInvite}
className="flex items-center gap-2 text-sm font-medium bg-indigo-600/20 text-indigo-400 hover:bg-indigo-600/30 hover:text-indigo-300 border border-indigo-500/30 px-4 py-2 rounded-lg transition-all"
>
<FontAwesomeIcon icon={faShareNodes} className="w-4 h-4" />
Invite Friends
</button>
);
}
Loading
Loading