From 4651c6b0844c699cda3c05ba84b9a82e36ba69b7 Mon Sep 17 00:00:00 2001 From: Jay Patrick Cano <0x3ef8@gmail.com> Date: Sun, 22 Mar 2026 11:44:33 +0800 Subject: [PATCH 1/5] fix errors --- app/(public)/leaderboard/[slug]/page.tsx | 16 +- .../[slug]/stats/StatsClientView.tsx | 162 +++++++++++ .../leaderboard/[slug]/stats/page.tsx | 70 +++++ app/(public)/leaderboard/page.tsx | 2 +- app/components/LeaderboardTable.tsx | 271 +++++++++--------- app/components/landing-page/TopLeaderbord.tsx | 13 +- app/components/leaderboard/BackButton.tsx | 4 +- .../leaderboard/InviteFriendsButton.tsx | 24 ++ 8 files changed, 416 insertions(+), 146 deletions(-) create mode 100644 app/(public)/leaderboard/[slug]/stats/StatsClientView.tsx create mode 100644 app/(public)/leaderboard/[slug]/stats/page.tsx create mode 100644 app/components/leaderboard/InviteFriendsButton.tsx diff --git a/app/(public)/leaderboard/[slug]/page.tsx b/app/(public)/leaderboard/[slug]/page.tsx index 24f6d83..aaf1279 100644 --- a/app/(public)/leaderboard/[slug]/page.tsx +++ b/app/(public)/leaderboard/[slug]/page.tsx @@ -1,7 +1,9 @@ import { createClient } from "../../../lib/supabase/server"; +import Link from "next/link"; import LeaderboardTable, { NonNullableMember } from "../../../components/LeaderboardTable"; import LeaderboardHeader from "@/app/components/leaderboard/Header"; import BackButton from "@/app/components/leaderboard/BackButton"; +import InviteFriendsButton from "@/app/components/leaderboard/InviteFriendsButton"; import Footer from "@/app/components/layout/Footer"; import CTA from "@/app/components/layout/CTA"; @@ -53,7 +55,19 @@ export default async function LeaderboardPage(props: { return (
- +
+ +
+ + + View Stats + + +
+
{ + setTimeout(() => { + AOS.refresh(); + setAnimated(true); + }, 200); + }, []); + + const totalSeconds = members.reduce((acc, m) => acc + (m.total_seconds || 0), 0); + const totalDevs = members.length; + + const languageTime: Record = {}; + const editorTime: Record = {}; + const osTime: Record = {}; + + members.forEach((m) => { + (m.languages as any[] || []).forEach((l) => { + languageTime[l.name] = (languageTime[l.name] || 0) + (l.total_seconds || 0); + }); + (m.editors as any[] || []).forEach((e) => { + editorTime[e.name] = (editorTime[e.name] || 0) + (e.total_seconds || 0); + }); + (m.operating_systems as any[] || []).forEach((os) => { + osTime[os.name] = (osTime[os.name] || 0) + (os.total_seconds || 0); + }); + }); + + const languageList = Object.entries(languageTime) + .map(([name, total_seconds]) => ({ + name, + total_seconds, + percent: (total_seconds / Math.max(totalSeconds, 1)) * 100 + })) + .sort((a, b) => b.total_seconds - a.total_seconds); + + const editorList = Object.entries(editorTime) + .map(([name, total_seconds]) => ({ + name, + total_seconds, + percent: (total_seconds / Math.max(totalSeconds, 1)) * 100 + })) + .sort((a, b) => b.total_seconds - a.total_seconds); + + const osList = Object.entries(osTime) + .map(([name, total_seconds]) => ({ + name, + total_seconds, + percent: (total_seconds / Math.max(totalSeconds, 1)) * 100 + })) + .sort((a, b) => b.total_seconds - a.total_seconds); + + const mockStatsData: StatsData = { + total_seconds: totalSeconds, + daily_average: totalSeconds / Math.max(totalDevs, 1), + languages: languageList, + editors: editorList, + operating_systems: osList, + }; + + const totalHoursFormatted = formatHours(totalSeconds); + const avgHoursFormatted = formatHours(mockStatsData.daily_average || 0); + const topLang = languageList[0]?.name || "N/A"; + const topEditor = editorList[0]?.name || "N/A"; + + const totalCodingProgress = Math.min(100, (totalSeconds / (40 * 3600 * Math.max(totalDevs, 1))) * 100); + const dailyAverageProgress = Math.min(100, (mockStatsData.daily_average / (8 * 3600)) * 100); + const topLangProgress = languageList[0]?.percent || 0; + const topEditorProgress = editorList[0]?.percent || 0; + + const sortedMembers = [...members].sort((a, b) => (b.total_seconds || 0) - (a.total_seconds || 0)); + + const statCards = [ + { + label: "Total Coding", + value: totalHoursFormatted, + sub: "Leaderboard Total", + color: "#6366f1", + trend: `${totalDevs} Dev${totalDevs !== 1 ? 's' : ''}`, + trendUp: true, + progress: totalCodingProgress || 50, + }, + { + label: "Avg Per Dev", + value: avgHoursFormatted, + sub: "Total average", + color: "#8b5cf6", + trend: `${dailyAverageProgress.toFixed(0)}%`, + trendUp: true, + progress: dailyAverageProgress || 50, + }, + { + label: "Top Language", + value: topLang, + sub: formatHours(languageList[0]?.total_seconds || 0), + color: "#22d3ee", + trend: `${topLangProgress.toFixed(0)}%`, + trendUp: true, + progress: topLangProgress, + }, + { + label: "Editor", + value: topEditor, + sub: formatHours(editorList[0]?.total_seconds || 0), + color: "#34d399", + trend: `${topEditorProgress.toFixed(0)}%`, + trendUp: true, + progress: topEditorProgress, + }, + { + label: "Top Member", + value: sortedMembers[0] ? (sortedMembers[0].email?.split("@")[0] || "Unknown") : "N/A", + sub: sortedMembers[0] ? formatHours(sortedMembers[0].total_seconds || 0) : "", + color: "#f59e0b", + trend: "1st", + trendUp: true, + progress: 100, + }, + ]; + + const pieData = languageList.slice(0, 6).map((l) => ({ + name: l.name, + value: l.total_seconds, + })); + + return ( +
+ {/* 5-Column Stats Cards (Matching Dashboard) */} + + + {/* 3-Column Chart Layout (Matching Dashboard) */} +
+
+ +
+ +
+ +
+ +
+ +
+
+
+ ); +} diff --git a/app/(public)/leaderboard/[slug]/stats/page.tsx b/app/(public)/leaderboard/[slug]/stats/page.tsx new file mode 100644 index 0000000..68139c9 --- /dev/null +++ b/app/(public)/leaderboard/[slug]/stats/page.tsx @@ -0,0 +1,70 @@ +import { createClient } from "@/app/lib/supabase/server"; +import { NonNullableMember } from "@/app/components/LeaderboardTable"; +import BackButton from "@/app/components/leaderboard/BackButton"; +import LeaderboardHeader from "@/app/components/leaderboard/Header"; +import Footer from "@/app/components/layout/Footer"; +import CTA from "@/app/components/layout/CTA"; +import StatsClientView from "./StatsClientView"; + +export default async function LeaderboardStatsPage({ + params, +}: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await params; + const supabase = await createClient(); + + const { data: leaderboard } = await supabase + .from("leaderboards") + .select("*") + .eq("slug", slug) + .single(); + + if (!leaderboard) { + return ( +
+
+

Leaderboard not found

+

{slug}

+
+
+ ); + } + + const { data: members, error } = await supabase + .from("leaderboard_members_view") + .select("*") + .eq("leaderboard_id", leaderboard.id); + + const { + data: { user }, + } = await supabase.auth.getUser(); + + const isOwner = user?.id === leaderboard.owner_id; + + if (error) { + return ( +
+
+

Error loading members.

+
+
+ ); + } + + return ( +
+
+
+ +
+ + + +
+ + {!user && } +
+
+ ); +} \ No newline at end of file diff --git a/app/(public)/leaderboard/page.tsx b/app/(public)/leaderboard/page.tsx index ba2285d..db786ff 100644 --- a/app/(public)/leaderboard/page.tsx +++ b/app/(public)/leaderboard/page.tsx @@ -37,7 +37,7 @@ export default async function Leaderboards() { return (
- +
DevPulse Logo diff --git a/app/components/LeaderboardTable.tsx b/app/components/LeaderboardTable.tsx index b3ada65..3a3e04d 100644 --- a/app/components/LeaderboardTable.tsx +++ b/app/components/LeaderboardTable.tsx @@ -8,6 +8,10 @@ import { faFire, faStar, faRocket, + faTrophy, + faGhost, + faMedal, + faCrown, } from "@fortawesome/free-solid-svg-icons"; import { Database } from "../supabase-types"; @@ -36,126 +40,101 @@ export type NonNullableMember = Omit< editors: { name: string }[]; }; -function LeaderboardStats({ members }: { members: NonNullableMember[] }) { - const totalHours = Math.round( - members.reduce((acc, m) => acc + (m.total_seconds || 0), 0) / 3600, - ); - - const languageCount: Record = {}; - const editorCount: Record = {}; - const osCount: Record = {}; - members.forEach((m) => { - (m.languages || []).forEach((l) => { - languageCount[l.name] = (languageCount[l.name] || 0) + 1; - }); - (m.editors || []).forEach((e) => { - editorCount[e.name] = (editorCount[e.name] || 0) + 1; - }); - (m.operating_systems || []).forEach((os) => { - osCount[os.name] = (osCount[os.name] || 0) + 1; - }); - }); +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: null }; + return { label: "", class: "hidden", icon: null }; // 0 hours +}; - const topLanguage = - Object.entries(languageCount).sort((a, b) => b[1] - a[1])[0]?.[0] || "N/A"; - const topEditor = - Object.entries(editorCount).sort((a, b) => b[1] - a[1])[0]?.[0] || "N/A"; - const topOS = - Object.entries(osCount).sort((a, b) => b[1] - a[1])[0]?.[0] || "N/A"; +function LeaderboardPodium({ topUsers }: { topUsers: any[] }) { + if (topUsers.length === 0) return null; return ( -
-
-
-
-
- -
- - Total Time - -
-
- {totalHours}{" "} - - hrs - -
-
+
+ {topUsers.map((user, idx) => { + const rankColor = + idx === 0 + ? "text-yellow-400 drop-shadow-[0_0_8px_rgba(250,204,21,0.4)]" + : idx === 1 + ? "text-gray-300 drop-shadow-[0_0_8px_rgba(209,213,219,0.4)]" + : "text-amber-600 drop-shadow-[0_0_8px_rgba(217,119,6,0.4)]"; + + const badgeInfo = getBadgeInfo(user.rank, user.hours); + const initial = (user.email?.[0] || "?").toUpperCase(); -
-
-
-
- -
- - Top Lang - -
-
- {topLanguage} -
-
+ return ( +
+ {/* Minimal Background Glow based on rank */} +
-
-
-
-
- -
- - Editor - -
-
- {topEditor} -
-
+
+
+
+ {initial} +
+
+
+ {user.email.split("@")[0]} +
+
+ {badgeInfo.icon && ( + + )} + {badgeInfo.label} +
+
+
+ +
+ + {idx === 0 ? "01" : idx === 1 ? "02" : "03"} + +
+
-
-
-
-
- + {/* Stats Row */} +
+
+ + Hours + + + {user.hours} + +
+
+ + Language + + + {user.languages?.[0] || "N/A"} + +
+
+ + Editor + + + {user.editor || "N/A"} + +
+
- - OS - -
-
- {topOS} -
-
+ ); + })}
); } -const getBadgeInfo = (rank: number, hours: number) => { - if (rank === 1) - return { label: "GOD LEVEL", class: "badge-god", icon: faBolt }; - if (rank === 2) return { label: "ELITE", class: "badge-elite", icon: faFire }; - if (rank === 3) return { label: "PRO", class: "badge-pro", icon: faStar }; - if (hours > 100) - return { label: "MASTER", class: "badge-master", icon: faRocket }; - if (hours > 20) - return { label: "HUSTLER", class: "badge-hustler", icon: null }; - return { label: "NOVICE", class: "badge-novice", icon: null }; -}; - export default function LeaderboardTable({ members, ownerId, @@ -208,29 +187,45 @@ export default function LeaderboardTable({ gap: 0.25rem; line-height: normal; } + .badge-impossible { + background: linear-gradient(90deg, rgba(255,0,102,0.15) 0%, rgba(255,51,153,0.8) 50%, rgba(255,0,102,0.15) 100%); + background-size: 200% auto; + animation: pulse-glow 1.5s infinite, shimmer-bg 1.5s linear infinite; + border: 1px solid rgba(255,0,102,0.8); + color: #ff99cc; + box-shadow: 0 0 15px rgba(255,0,102,0.6); + } .badge-god { - background: linear-gradient(90deg, rgba(234,179,8,0.15) 0%, rgba(253,224,71,0.6) 50%, rgba(234,179,8,0.15) 100%); + background: linear-gradient(90deg, rgba(217,70,239,0.15) 0%, rgba(232,121,249,0.6) 50%, rgba(217,70,239,0.15) 100%); background-size: 200% auto; - animation: shimmer-bg 2s linear infinite; - border: 1px solid rgba(250,204,21,0.6); - color: #fef08a; - box-shadow: 0 0 10px rgba(250,204,21,0.3); + animation: shimmer-bg 2s linear infinite, glow-pulse 2s alternate infinite; + border: 1px solid rgba(217,70,239,0.6); + color: #f0abfc; + box-shadow: 0 0 10px rgba(217,70,239,0.4); } - .badge-elite { - background: linear-gradient(90deg, rgba(148,163,184,0.15) 0%, rgba(241,245,249,0.5) 50%, rgba(148,163,184,0.15) 100%); + .badge-starlight { + background: linear-gradient(90deg, rgba(56,189,248,0.15) 0%, rgba(125,211,252,0.6) 50%, rgba(56,189,248,0.15) 100%); background-size: 200% auto; animation: shimmer-bg 2.5s linear infinite; - border: 1px solid rgba(203,213,225,0.5); - color: #ffffff; - box-shadow: 0 0 10px rgba(203,213,225,0.2); + border: 1px solid rgba(56,189,248,0.5); + color: #bae6fd; + box-shadow: 0 0 10px rgba(56,189,248,0.3); } - .badge-pro { - background: linear-gradient(90deg, rgba(217,119,6,0.15) 0%, rgba(252,211,77,0.5) 50%, rgba(217,119,6,0.15) 100%); + .badge-elite { + background: linear-gradient(90deg, rgba(239,68,68,0.15) 0%, rgba(248,113,113,0.6) 50%, rgba(239,68,68,0.15) 100%); background-size: 200% auto; animation: shimmer-bg 3s linear infinite; - border: 1px solid rgba(217,119,6,0.5); - color: #fce68a; - box-shadow: 0 0 10px rgba(217,119,6,0.2); + border: 1px solid rgba(239,68,68,0.5); + color: #fca5a5; + box-shadow: 0 0 10px rgba(239,68,68,0.3); + } + .badge-pro { + background: linear-gradient(90deg, rgba(234,179,8,0.15) 0%, rgba(253,224,71,0.5) 50%, rgba(234,179,8,0.15) 100%); + background-size: 200% auto; + animation: shimmer-bg 4s linear infinite; + border: 1px solid rgba(234,179,8,0.5); + color: #fef08a; + box-shadow: 0 0 10px rgba(234,179,8,0.2); } .badge-master { background: rgba(99,102,241,0.15); @@ -243,14 +238,30 @@ export default function LeaderboardTable({ color: #93c5fd; } .badge-novice { + background: linear-gradient(90deg, rgba(16,185,129,0.1) 0%, rgba(52,211,153,0.3) 50%, rgba(16,185,129,0.1) 100%); + background-size: 200% auto; + animation: shimmer-bg 5s linear infinite; + border: 1px solid rgba(16,185,129,0.4); + color: #6ee7b7; + } + .badge-newbie { background: rgba(107,114,128,0.15); border: 1px solid rgba(107,114,128,0.4); color: #d1d5db; } + @keyframes pulse-glow { + 0%, 100% { box-shadow: 0 0 15px rgba(255,0,102,0.6); } + 50% { box-shadow: 0 0 25px rgba(255,0,102,1); } + } + @keyframes glow-pulse { + 0% { box-shadow: 0 0 5px rgba(217,70,239,0.4); } + 100% { box-shadow: 0 0 15px rgba(217,70,239,0.8); } + } `, }} /> - + + {ranked.length === 0 ? (
@@ -264,14 +275,14 @@ export default function LeaderboardTable({
Rank
Developer
-
Tech Stack
-
Environment
-
Score
+
Language
+
Editor
+
Hours
{/* List Body */}
- {ranked.map((user) => { + {ranked.slice(3).map((user) => { const isCurrentUser = user.user_id === ownerId; const pct = Math.max(2, (user.hours / maxHours) * 100); const badgeInfo = getBadgeInfo(user.rank, user.hours); @@ -346,7 +357,7 @@ export default function LeaderboardTable({ {/* MOBILE BOTTOM STACK / DESKTOP RIGHT ROW */}
- {/* Tech Stack */} + {/* Language */}
{user.languages.length > 0 ? ( user.languages.map((lang, i) => ( @@ -364,7 +375,7 @@ export default function LeaderboardTable({ )}
- {/* Environment */} + {/* Editor */}
{user.editor !== "N/A" && ( diff --git a/app/components/landing-page/TopLeaderbord.tsx b/app/components/landing-page/TopLeaderbord.tsx index 52db441..af33bbd 100644 --- a/app/components/landing-page/TopLeaderbord.tsx +++ b/app/components/landing-page/TopLeaderbord.tsx @@ -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({ diff --git a/app/components/leaderboard/BackButton.tsx b/app/components/leaderboard/BackButton.tsx index 9029387..cff840a 100644 --- a/app/components/leaderboard/BackButton.tsx +++ b/app/components/leaderboard/BackButton.tsx @@ -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 ( + ); +} From 352c1d862fb14da168619da5a75d756a994c9230 Mon Sep 17 00:00:00 2001 From: Jay Patrick Cano <0x3ef8@gmail.com> Date: Sun, 22 Mar 2026 11:51:54 +0800 Subject: [PATCH 2/5] fix missing use client in Conversations.tsx --- app/components/chat/Conversations.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/components/chat/Conversations.tsx b/app/components/chat/Conversations.tsx index 4ca8cdc..a675119 100644 --- a/app/components/chat/Conversations.tsx +++ b/app/components/chat/Conversations.tsx @@ -1,3 +1,4 @@ +"use client"; import { User } from "@supabase/supabase-js"; import { Conversation } from "../Chat"; @@ -32,3 +33,4 @@ export default function Conversations({ ); } + From 2f23c4ada8ae2538daafbe97ccc53bfe30e5a137 Mon Sep 17 00:00:00 2001 From: Jay Patrick Cano <0x3ef8@gmail.com> Date: Sun, 22 Mar 2026 11:51:58 +0800 Subject: [PATCH 3/5] fix missing use client in StatsCard.tsx --- app/components/dashboard/widgets/StatsCard.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/components/dashboard/widgets/StatsCard.tsx b/app/components/dashboard/widgets/StatsCard.tsx index b9a73aa..efc5ea3 100644 --- a/app/components/dashboard/widgets/StatsCard.tsx +++ b/app/components/dashboard/widgets/StatsCard.tsx @@ -1,3 +1,4 @@ +"use client"; export interface StatCard { label: string; value: string; @@ -72,3 +73,4 @@ export default function StatsCard({
); } + From e3b5d497b6aff87d8a96d82118ec790d972380bd Mon Sep 17 00:00:00 2001 From: Jay Patrick Cano <0x3ef8@gmail.com> Date: Sun, 22 Mar 2026 14:58:04 +0800 Subject: [PATCH 4/5] update leaderboard --- app/(public)/leaderboard/[slug]/page.tsx | 28 +- .../[slug]/stats/StatsClientView.tsx | 162 ------- .../leaderboard/[slug]/stats/page.tsx | 70 --- app/components/Chat.tsx | 1 - app/components/LeaderboardTable.tsx | 94 +++- app/components/chat/Messages.tsx | 8 +- app/components/dashboard/Navbar.tsx | 1 - app/components/leaderboard/BackButton.tsx | 2 +- app/components/leaderboard/Banner.tsx | 41 ++ app/components/leaderboard/Header.tsx | 64 ++- .../leaderboard/InviteFriendsButton.tsx | 14 +- .../leaderboard/LeaderboardStats.tsx | 143 ++++++ package-lock.json | 454 +++++++++++++----- package.json | 1 + 14 files changed, 666 insertions(+), 417 deletions(-) delete mode 100644 app/(public)/leaderboard/[slug]/stats/StatsClientView.tsx delete mode 100644 app/(public)/leaderboard/[slug]/stats/page.tsx create mode 100644 app/components/leaderboard/Banner.tsx create mode 100644 app/components/leaderboard/LeaderboardStats.tsx diff --git a/app/(public)/leaderboard/[slug]/page.tsx b/app/(public)/leaderboard/[slug]/page.tsx index aaf1279..ebfdfc4 100644 --- a/app/(public)/leaderboard/[slug]/page.tsx +++ b/app/(public)/leaderboard/[slug]/page.tsx @@ -1,9 +1,6 @@ import { createClient } from "../../../lib/supabase/server"; -import Link from "next/link"; import LeaderboardTable, { NonNullableMember } from "../../../components/LeaderboardTable"; import LeaderboardHeader from "@/app/components/leaderboard/Header"; -import BackButton from "@/app/components/leaderboard/BackButton"; -import InviteFriendsButton from "@/app/components/leaderboard/InviteFriendsButton"; import Footer from "@/app/components/layout/Footer"; import CTA from "@/app/components/layout/CTA"; @@ -54,25 +51,14 @@ export default async function LeaderboardPage(props: { return (
-
-
- -
- - - View Stats - - -
-
+
- +
+ +
{!user && } diff --git a/app/(public)/leaderboard/[slug]/stats/StatsClientView.tsx b/app/(public)/leaderboard/[slug]/stats/StatsClientView.tsx deleted file mode 100644 index f1bf410..0000000 --- a/app/(public)/leaderboard/[slug]/stats/StatsClientView.tsx +++ /dev/null @@ -1,162 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import AOS from "aos"; -import "devicon/devicon.min.css"; -import { NonNullableMember } from "@/app/components/LeaderboardTable"; -import { formatHours } from "@/app/utils/time"; - -import StatsCard from "@/app/components/dashboard/widgets/StatsCard"; -import LanguageDestribution from "@/app/components/dashboard/widgets/LanguageDestribution"; -import Editors from "@/app/components/dashboard/widgets/Editors"; -import OperatingSystem from "@/app/components/dashboard/widgets/OperatingSystem"; -import { StatsData } from "@/app/components/dashboard/Stats"; - -export default function StatsClientView({ members }: { members: NonNullableMember[] }) { - const [animated, setAnimated] = useState(false); - - useEffect(() => { - setTimeout(() => { - AOS.refresh(); - setAnimated(true); - }, 200); - }, []); - - const totalSeconds = members.reduce((acc, m) => acc + (m.total_seconds || 0), 0); - const totalDevs = members.length; - - const languageTime: Record = {}; - const editorTime: Record = {}; - const osTime: Record = {}; - - members.forEach((m) => { - (m.languages as any[] || []).forEach((l) => { - languageTime[l.name] = (languageTime[l.name] || 0) + (l.total_seconds || 0); - }); - (m.editors as any[] || []).forEach((e) => { - editorTime[e.name] = (editorTime[e.name] || 0) + (e.total_seconds || 0); - }); - (m.operating_systems as any[] || []).forEach((os) => { - osTime[os.name] = (osTime[os.name] || 0) + (os.total_seconds || 0); - }); - }); - - const languageList = Object.entries(languageTime) - .map(([name, total_seconds]) => ({ - name, - total_seconds, - percent: (total_seconds / Math.max(totalSeconds, 1)) * 100 - })) - .sort((a, b) => b.total_seconds - a.total_seconds); - - const editorList = Object.entries(editorTime) - .map(([name, total_seconds]) => ({ - name, - total_seconds, - percent: (total_seconds / Math.max(totalSeconds, 1)) * 100 - })) - .sort((a, b) => b.total_seconds - a.total_seconds); - - const osList = Object.entries(osTime) - .map(([name, total_seconds]) => ({ - name, - total_seconds, - percent: (total_seconds / Math.max(totalSeconds, 1)) * 100 - })) - .sort((a, b) => b.total_seconds - a.total_seconds); - - const mockStatsData: StatsData = { - total_seconds: totalSeconds, - daily_average: totalSeconds / Math.max(totalDevs, 1), - languages: languageList, - editors: editorList, - operating_systems: osList, - }; - - const totalHoursFormatted = formatHours(totalSeconds); - const avgHoursFormatted = formatHours(mockStatsData.daily_average || 0); - const topLang = languageList[0]?.name || "N/A"; - const topEditor = editorList[0]?.name || "N/A"; - - const totalCodingProgress = Math.min(100, (totalSeconds / (40 * 3600 * Math.max(totalDevs, 1))) * 100); - const dailyAverageProgress = Math.min(100, (mockStatsData.daily_average / (8 * 3600)) * 100); - const topLangProgress = languageList[0]?.percent || 0; - const topEditorProgress = editorList[0]?.percent || 0; - - const sortedMembers = [...members].sort((a, b) => (b.total_seconds || 0) - (a.total_seconds || 0)); - - const statCards = [ - { - label: "Total Coding", - value: totalHoursFormatted, - sub: "Leaderboard Total", - color: "#6366f1", - trend: `${totalDevs} Dev${totalDevs !== 1 ? 's' : ''}`, - trendUp: true, - progress: totalCodingProgress || 50, - }, - { - label: "Avg Per Dev", - value: avgHoursFormatted, - sub: "Total average", - color: "#8b5cf6", - trend: `${dailyAverageProgress.toFixed(0)}%`, - trendUp: true, - progress: dailyAverageProgress || 50, - }, - { - label: "Top Language", - value: topLang, - sub: formatHours(languageList[0]?.total_seconds || 0), - color: "#22d3ee", - trend: `${topLangProgress.toFixed(0)}%`, - trendUp: true, - progress: topLangProgress, - }, - { - label: "Editor", - value: topEditor, - sub: formatHours(editorList[0]?.total_seconds || 0), - color: "#34d399", - trend: `${topEditorProgress.toFixed(0)}%`, - trendUp: true, - progress: topEditorProgress, - }, - { - label: "Top Member", - value: sortedMembers[0] ? (sortedMembers[0].email?.split("@")[0] || "Unknown") : "N/A", - sub: sortedMembers[0] ? formatHours(sortedMembers[0].total_seconds || 0) : "", - color: "#f59e0b", - trend: "1st", - trendUp: true, - progress: 100, - }, - ]; - - const pieData = languageList.slice(0, 6).map((l) => ({ - name: l.name, - value: l.total_seconds, - })); - - return ( -
- {/* 5-Column Stats Cards (Matching Dashboard) */} - - - {/* 3-Column Chart Layout (Matching Dashboard) */} -
-
- -
- -
- -
- -
- -
-
-
- ); -} diff --git a/app/(public)/leaderboard/[slug]/stats/page.tsx b/app/(public)/leaderboard/[slug]/stats/page.tsx deleted file mode 100644 index 68139c9..0000000 --- a/app/(public)/leaderboard/[slug]/stats/page.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { createClient } from "@/app/lib/supabase/server"; -import { NonNullableMember } from "@/app/components/LeaderboardTable"; -import BackButton from "@/app/components/leaderboard/BackButton"; -import LeaderboardHeader from "@/app/components/leaderboard/Header"; -import Footer from "@/app/components/layout/Footer"; -import CTA from "@/app/components/layout/CTA"; -import StatsClientView from "./StatsClientView"; - -export default async function LeaderboardStatsPage({ - params, -}: { - params: Promise<{ slug: string }>; -}) { - const { slug } = await params; - const supabase = await createClient(); - - const { data: leaderboard } = await supabase - .from("leaderboards") - .select("*") - .eq("slug", slug) - .single(); - - if (!leaderboard) { - return ( -
-
-

Leaderboard not found

-

{slug}

-
-
- ); - } - - const { data: members, error } = await supabase - .from("leaderboard_members_view") - .select("*") - .eq("leaderboard_id", leaderboard.id); - - const { - data: { user }, - } = await supabase.auth.getUser(); - - const isOwner = user?.id === leaderboard.owner_id; - - if (error) { - return ( -
-
-

Error loading members.

-
-
- ); - } - - return ( -
-
-
- -
- - - -
- - {!user && } -
-
- ); -} \ No newline at end of file diff --git a/app/components/Chat.tsx b/app/components/Chat.tsx index 30a4260..06bdcc6 100644 --- a/app/components/Chat.tsx +++ b/app/components/Chat.tsx @@ -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; diff --git a/app/components/LeaderboardTable.tsx b/app/components/LeaderboardTable.tsx index 3a3e04d..d772cd2 100644 --- a/app/components/LeaderboardTable.tsx +++ b/app/components/LeaderboardTable.tsx @@ -1,17 +1,13 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { - faClock, - faCode, - faTerminal, - faServer, faBolt, faFire, faStar, - faRocket, - faTrophy, faGhost, faMedal, faCrown, + faSeedling, + faMinus, } from "@fortawesome/free-solid-svg-icons"; import { Database } from "../supabase-types"; @@ -48,11 +44,11 @@ const getBadgeInfo = (rank: number, hours: number) => { 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: null }; - return { label: "", class: "hidden", icon: null }; // 0 hours + if (hours >= 1) return { label: "NEWBIE", class: "badge-newbie", icon: faSeedling }; + return { label: "NONE", class: "badge-none", icon: faMinus }; // 0 hours }; -function LeaderboardPodium({ topUsers }: { topUsers: any[] }) { +function LeaderboardPodium({ topUsers }: { topUsers: { user_id: string; rank: number; email: string | null; hours: number; role: string | null; languages: string[]; os: string; editor: string; }[] }) { if (topUsers.length === 0) return null; return ( @@ -83,7 +79,7 @@ function LeaderboardPodium({ topUsers }: { topUsers: any[] }) {
- {user.email.split("@")[0]} + {user.email?.split("@")[0] || "Unknown"}
{badgeInfo.icon && ( @@ -95,7 +91,7 @@ function LeaderboardPodium({ topUsers }: { topUsers: any[] }) {
- + {idx === 0 ? "01" : idx === 1 ? "02" : "03"}
@@ -135,6 +131,8 @@ function LeaderboardPodium({ topUsers }: { topUsers: any[] }) { ); } +import LeaderboardStats from "./leaderboard/LeaderboardStats"; + export default function LeaderboardTable({ members, ownerId, @@ -167,7 +165,7 @@ export default function LeaderboardTable({ }; return ( -
+