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
16 changes: 12 additions & 4 deletions crackcode/client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

166 changes: 131 additions & 35 deletions crackcode/client/src/components/leaderboard/leaderboardTable.jsx
Original file line number Diff line number Diff line change
@@ -1,68 +1,164 @@
import React from "react";
import { UserRoundSearch, Flame } from "lucide-react";

// ── Renders either an <img> or emoji depending on the avatar value ──
const Avatar = ({ avatar, name }) => {
const isImagePath =
typeof avatar === "string" &&
(avatar.startsWith("/") ||
avatar.startsWith("http") ||
avatar.match(/\.(png|jpg|jpeg|gif|webp|svg)$/i));

if (isImagePath) {
return (
<img
src={avatar}
alt={name}
style={{ width: 32, height: 32, borderRadius: "50%", objectFit: "cover",
border: "2px solid #334155", flexShrink: 0 }}
onError={(e) => { e.target.replaceWith(Object.assign(document.createElement("span"), { textContent: "🕵️" })); }}
style={{
width: 32, height: 32, borderRadius: "50%", objectFit: "cover",
border: "2px solid var(--border)", flexShrink: 0,
}}
onError={(e) => { e.target.style.display = "none"; }}
/>
);
}

const isEmoji = typeof avatar === "string" && /\p{Emoji}/u.test(avatar);
return <span className="text-xl">{isEmoji ? avatar : <UserRoundSearch className="w-6 h-6" />}</span>;
return (
<span style={{ fontSize: "1.3rem" }}>
{isEmoji ? avatar : <UserRoundSearch style={{ width: 24, height: 24 }} />}
</span>
);
};

const RANK_MEDAL = { 1: "🥇", 2: "🥈", 3: "🥉" };

const LeaderboardTable = ({ data = [] }) => {
return (
<div className="table-container" style={{ width: "100%" }}>
<div
style={{
width: "100%",
borderRadius: "1rem",
border: "1px solid var(--border)",
background: "var(--surface)",
overflow: "hidden",
marginTop: "2rem",
}}
>
<table style={{ width: "100%", tableLayout: "fixed", borderSpacing: 0, borderCollapse: "collapse" }}>

{/* Header */}
<thead>
<tr>
<th style={{ width: "6%", padding: "18px 20px", textAlign: "left" }}>Rank</th>
<th style={{ width: "20%", padding: "18px 20px", textAlign: "left" }}>Detective</th>
<th style={{ width: "14%", padding: "18px 20px", textAlign: "left" }}>Title</th>
<th style={{ width: "18%", padding: "18px 20px", textAlign: "left" }}>Specialization</th>
<th style={{ width: "16%", padding: "18px 20px", textAlign: "left" }}>Investigation Points</th>
<th style={{ width: "13%", padding: "18px 20px", textAlign: "left" }}>Cases Solved</th>
<th style={{ width: "13%", padding: "18px 20px", textAlign: "left" }}>Streak</th>
<tr style={{ borderBottom: "1px solid var(--border)" }}>
{[
{ label: "Rank", width: "7%" },
{ label: "Detective", width: "22%" },
{ label: "Title", width: "13%" },
{ label: "Specialization", width: "16%" },
{ label: "Investigation Points", width: "17%" },
{ label: "Cases Solved", width: "13%" },
{ label: "Streak", width: "12%" },
].map(({ label, width }) => (
<th
key={label}
style={{
width,
padding: "14px 20px",
textAlign: "left",
fontSize: "0.7rem",
fontWeight: 700,
letterSpacing: "0.08em",
textTransform: "uppercase",
color: "var(--muted)",
}}
>
{label}
</th>
))}
</tr>
</thead>

{/* Body */}
<tbody>
{data.map((user) => (
<tr key={user.rank} style={{ borderTop: "1px solid rgba(255,255,255,0.06)" }}>
<td style={{ padding: "20px 20px" }}>#{user.rank}</td>
<td style={{ padding: "20px 20px" }}>
<div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
<Avatar avatar={user.avatar} name={user.name} />
<span>{user.name}</span>
</div>
</td>
<td style={{ padding: "20px 20px" }}>{user.title}</td>
<td style={{ padding: "20px 20px" }}>{user.specialization}</td>
<td style={{ padding: "20px 20px" }} className="green">{user.points.toLocaleString()}</td>
<td style={{ padding: "20px 20px" }}>{user.cases}</td>
<td style={{ padding: "20px 20px" }} className="orange">
<div className="flex items-center gap-1">
{user.streak} <Flame className="w-4 h-4" />
</div>
</td>
</tr>
))}
{data.map((user, i) => {
const isTop3 = user.rank <= 3;
return (
<tr
key={user.rank}
style={{
borderTop: i === 0 ? "none" : "1px solid var(--border)",
background: isTop3 ? "rgba(var(--brand-rgb, 202,138,4), 0.04)" : "transparent",
transition: "background 0.15s",
}}
onMouseEnter={(e) => (e.currentTarget.style.background = "rgba(128,128,128,0.07)")}
onMouseLeave={(e) => (e.currentTarget.style.background = isTop3 ? "rgba(202,138,4,0.04)" : "transparent")}
>
{/* Rank */}
<td style={{ padding: "18px 20px", fontWeight: 700, color: "var(--text)" }}>
<span style={{ display: "flex", alignItems: "center", gap: 6 }}>
#{user.rank}
{RANK_MEDAL[user.rank] && (
<span style={{ fontSize: "1rem" }}>{RANK_MEDAL[user.rank]}</span>
)}
</span>
</td>

{/* Detective */}
<td style={{ padding: "18px 20px" }}>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<Avatar avatar={user.avatar} name={user.name} />
<span style={{ fontWeight: 600, color: "var(--text)" }}>{user.name}</span>
</div>
</td>

{/* Title badge */}
<td style={{ padding: "18px 20px" }}>
<span
style={{
display: "inline-block",
padding: "3px 10px",
borderRadius: 999,
fontSize: "0.72rem",
fontWeight: 700,
letterSpacing: "0.05em",
textTransform: "uppercase",
border: "1px solid var(--border)",
color: "var(--brand)",
background: "rgba(202,138,4,0.08)",
}}
>
{user.title}
</span>
</td>

{/* Specialization */}
<td style={{ padding: "18px 20px", color: "var(--muted)" }}>
{user.specialization}
</td>

{/* Investigation Points */}
<td style={{ padding: "18px 20px", fontWeight: 700, color: "var(--brand)" }}>
{user.points.toLocaleString()}
</td>

{/* Cases Solved */}
<td style={{ padding: "18px 20px", color: "var(--muted)" }}>
{user.cases}
</td>

{/* Streak */}
<td style={{ padding: "18px 20px" }}>
<div style={{ display: "flex", alignItems: "center", gap: 4 }}>
<Flame style={{ width: 16, height: 16, color: "#ea580c" }} />
<span style={{ fontWeight: 600, color: "#ea580c" }}>{user.streak}</span>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
};

export default LeaderboardTable;
export default LeaderboardTable;
4 changes: 2 additions & 2 deletions crackcode/client/src/pages/leaderboard/leaderboardPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,10 @@ const LeaderboardPage = () => {
<div className="min-h-screen flex flex-col" style={{ background: 'var(--bg)', color: 'var(--text)' }}>
<Header variant="empty" showBackBtn={false}/>

<main className="flex-1 px-6 sm:px-10 py-10 mt-20">
<main className="flex-1 px-6 sm:px-10 py-8 mt-15">

{/* Title + Filter buttons */}
<div className="flex items-center justify-between max-w-5xl mx-auto mt-20 mb-12">
<div className="flex items-center justify-between max-w-5xl mx-auto mt-12 mb-10">
<div className="text-center flex-1">
<div className="flex flex-col gap-5">
<h1
Expand Down
5 changes: 5 additions & 0 deletions crackcode/server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.