Skip to content

update leaderboard#38

Merged
mrepol742 merged 5 commits intohallofcodes:masterfrom
0x3EF8:update-leaderboard
Mar 23, 2026
Merged

update leaderboard#38
mrepol742 merged 5 commits intohallofcodes:masterfrom
0x3EF8:update-leaderboard

Conversation

@0x3EF8
Copy link
Member

@0x3EF8 0x3EF8 commented Mar 22, 2026

update leaderboard

Copilot AI review requested due to automatic review settings March 22, 2026 06:58
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refreshes the leaderboard UI/UX by introducing a new header/banner layout, richer leaderboard visuals (podium + badges + legend), and a stats sidebar, along with a few related client-component and typing adjustments.

Changes:

  • Add new leaderboard UI components (Banner, InviteFriendsButton, LeaderboardStats) and update the leaderboard header/layout.
  • Redesign leaderboard ranking presentation with a top-3 podium, updated badge tiers, and a rankings legend.
  • Dependency/lockfile updates (adds shx) and minor cleanup/typing tweaks in chat/dashboard components.

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
package.json Adds shx to devDependencies.
package-lock.json Updates lockfile to include shx and related dependency graph changes.
app/components/leaderboard/LeaderboardStats.tsx New leaderboard stats sidebar component (AOS + derived aggregates).
app/components/leaderboard/InviteFriendsButton.tsx New “Invite Friends” clipboard/share button.
app/components/leaderboard/Header.tsx New header layout with banner, back button overlay, and invite action.
app/components/leaderboard/Banner.tsx New banner component (image or generated gradient).
app/components/leaderboard/BackButton.tsx Adds configurable href prop; updates navigation target.
app/components/landing-page/TopLeaderbord.tsx Simplifies categories typing shape on TopMember.
app/components/dashboard/widgets/StatsCard.tsx Marks component as client-side.
app/components/dashboard/Navbar.tsx Removes an unused icon import.
app/components/chat/Messages.tsx Adjusts SyntaxHighlighter typing/spread; edits code block rendering.
app/components/chat/Conversations.tsx Marks component as client-side.
app/components/LeaderboardTable.tsx Major leaderboard table redesign: podium, badge tiers/styles, legend, layout changes.
app/components/Chat.tsx Removes unused toast import.
app/(public)/leaderboard/page.tsx Updates BackButton destination for the public leaderboard list page.
app/(public)/leaderboard/[slug]/page.tsx Removes standalone BackButton and updates page layout/wrapping.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 55 to +57
<>
{/* @ts-expect-error atomDark style type not compatible with SyntaxHighlighter */}
{ }

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.
Comment on lines +40 to +49
const getBadgeInfo = (rank: number, hours: number) => {
if (hours >= 160) return { label: "MISSION IMPOSSIBLE", class: "badge-impossible", icon: faGhost };
if (hours >= 130) return { label: "GOD LEVEL", class: "badge-god", icon: faCrown };
if (hours >= 100) return { label: "STARLIGHT", class: "badge-starlight", icon: faStar };
if (hours >= 50) return { label: "ELITE", class: "badge-elite", icon: faFire };
if (hours >= 20) return { label: "PRO", class: "badge-pro", icon: faBolt };
if (hours >= 5) return { label: "NOVICE", class: "badge-novice", icon: faMedal };
if (hours >= 1) return { label: "NEWBIE", class: "badge-newbie", icon: faSeedling };
return { label: "NONE", class: "badge-none", icon: faMinus }; // 0 hours
};
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.

rank is not used in getBadgeInfo, which may be flagged by lint rules and makes the API misleading. Either remove the parameter or incorporate rank-based logic if it’s intended to matter.

Copilot uses AI. Check for mistakes.
"@types/react-syntax-highlighter": "^15.5.13",
"eslint": "^9",
"eslint-config-next": "16.1.6",
"shx": "^0.4.0",
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.

shx is added to devDependencies, but there are no package.json scripts (or other repo references) using it. If it’s not needed, remove it; otherwise add/update the script(s) that rely on shx so the dependency is justified.

Suggested change
"shx": "^0.4.0",

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +19
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!");
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.
Comment on lines +76 to +77
{/* 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" />
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.
Comment on lines +31 to +39
(m.languages as { name?: string; total_seconds?: number }[] || []).forEach((l: { name?: string; total_seconds?: number } | string) => {
const name = typeof l === "string" ? l : l.name || "Unknown";
const secs = typeof l === "string" ? 3600 : l.total_seconds || 3600;
languageTime[name] = (languageTime[name] || 0) + secs;
});
(m.editors as { name?: string; total_seconds?: number }[] || []).forEach((e: { name?: string; total_seconds?: number } | string) => {
const name = typeof e === "string" ? e : e.name || "Unknown";
const secs = typeof e === "string" ? 3600 : e.total_seconds || 3600;
editorTime[name] = (editorTime[name] || 0) + secs;
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.

NonNullableMember.languages / editors are typed as { name: string }[] (no total_seconds). Defaulting each entry to 3600 seconds will significantly overcount and makes the “Top Language/Editor” totals and percentages inaccurate. Either extend the view/type to include per-language/editor total_seconds, or change this logic to compute counts (or derive time from a source that actually has per-language/editor durations).

Suggested change
(m.languages as { name?: string; total_seconds?: number }[] || []).forEach((l: { name?: string; total_seconds?: number } | string) => {
const name = typeof l === "string" ? l : l.name || "Unknown";
const secs = typeof l === "string" ? 3600 : l.total_seconds || 3600;
languageTime[name] = (languageTime[name] || 0) + secs;
});
(m.editors as { name?: string; total_seconds?: number }[] || []).forEach((e: { name?: string; total_seconds?: number } | string) => {
const name = typeof e === "string" ? e : e.name || "Unknown";
const secs = typeof e === "string" ? 3600 : e.total_seconds || 3600;
editorTime[name] = (editorTime[name] || 0) + secs;
const memberSeconds = m.total_seconds || 0;
const languages = ((m.languages as ({ name?: string } | string)[] | undefined) || []);
const perLanguageSeconds = languages.length > 0 ? memberSeconds / languages.length : 0;
languages.forEach((l) => {
const name = typeof l === "string" ? l : l.name || "Unknown";
languageTime[name] = (languageTime[name] || 0) + perLanguageSeconds;
});
const editors = ((m.editors as ({ name?: string } | string)[] | undefined) || []);
const perEditorSeconds = editors.length > 0 ? memberSeconds / editors.length : 0;
editors.forEach((e) => {
const name = typeof e === "string" ? e : e.name || "Unknown";
editorTime[name] = (editorTime[name] || 0) + perEditorSeconds;

Copilot uses AI. Check for mistakes.
import { useEffect } from "react";
import AOS from "aos";
import "aos/dist/aos.css";
import { NonNullableMember } from "../LeaderboardTable";
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 imports NonNullableMember as a value import, creating a circular module dependency (LeaderboardTable imports LeaderboardStats, and LeaderboardStats imports from LeaderboardTable). Make this a type-only import (import type ...) and/or move shared types to a separate module to avoid runtime circular import issues.

Suggested change
import { NonNullableMember } from "../LeaderboardTable";
import type { NonNullableMember } from "../LeaderboardTable";

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +18
setTimeout(() => {
AOS.refresh();
}, 200);
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 setTimeout in this effect is never cleared. If the component unmounts quickly (navigation), it can still fire and call AOS.refresh(). Store the timeout id and clear it in the cleanup function.

Suggested change
setTimeout(() => {
AOS.refresh();
}, 200);
const timeoutId = setTimeout(() => {
AOS.refresh();
}, 200);
return () => {
clearTimeout(timeoutId);
};

Copilot uses AI. Check for mistakes.
Comment on lines 228 to 237
@@ -243,18 +236,42 @@ export default function LeaderboardTable({
color: #93c5fd;
}
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.

getBadgeInfo no longer returns badge-master or badge-hustler, but the CSS for these classes remains. Removing unused badge styles (or updating getBadgeInfo to use them) will keep the inline stylesheet smaller and easier to maintain.

Copilot uses AI. Check for mistakes.
@mrepol742 mrepol742 merged commit 983eb89 into hallofcodes:master Mar 23, 2026
5 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants