Skip to content
Merged
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 { } inside the fragment evaluates to an empty object and will be rendered as a React child, which triggers a runtime error (“Objects are not valid as a React child”). Remove this expression entirely.

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;
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-codes a “temporary placeholder” Unsplash banner URL in the UI. If this is meant to be temporary, it’s easy to forget and ship; consider falling back to the generated gradient (omit imageUrl) or sourcing the banner URL from leaderboard data/config instead of inlining it here.

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" />
<Banner name={leaderboard.name} />

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 +8 to +19
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 reject (e.g., non-secure context, permissions denied, unsupported browser). Consider awaiting it and handling errors (showing a toast on failure) and/or guarding for navigator.clipboard before calling.

Suggested change
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!");
const handleInvite = async () => {
if (typeof window === "undefined") {
return;
}
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}`;
if (!navigator.clipboard || typeof navigator.clipboard.writeText !== "function") {
toast.error("Clipboard is not available in this browser.");
return;
}
try {
await navigator.clipboard.writeText(message);
toast.success("Invite message copied to clipboard!");
} catch (_error) {
toast.error("Failed to copy invite message to clipboard.");

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