Skip to content

Commit 98ad5bf

Browse files
Refactor color mode handling to use useSafeColorMode; add badge display functionality in leaderboard component
1 parent 14ebe21 commit 98ad5bf

File tree

13 files changed

+245
-34
lines changed

13 files changed

+245
-34
lines changed

src/components/dashboard/LeaderBoard/PRListModal.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import React, { useEffect } from "react";
33
import { motion, AnimatePresence } from "framer-motion";
44
import { FaTimes, FaExternalLinkAlt, FaGithub } from "react-icons/fa";
5-
import { useColorMode } from "@docusaurus/theme-common";
5+
import { useSafeColorMode } from "@site/src/utils/useSafeColorMode";
66
import { useCommunityStatsContext } from "../../../lib/statsProvider";
77

88
interface PRDetails {
@@ -34,8 +34,7 @@ export default function PRListModal({
3434
isOpen,
3535
onClose,
3636
}: PRListModalProps): JSX.Element | null {
37-
const { colorMode } = useColorMode();
38-
const isDark = colorMode === "dark";
37+
const { isDark } = useSafeColorMode();
3938

4039
// Get filtered PRs from context
4140
const { getFilteredPRsForContributor, currentTimeFilter } =

src/components/dashboard/LeaderBoard/leaderboard.css

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,7 @@
546546

547547
.contributors-header {
548548
display: grid;
549-
grid-template-columns: 0.5fr 0.5fr 2fr 1fr 1fr;
549+
grid-template-columns: 0.5fr 0.5fr 2fr 1fr 1fr 1.5fr;
550550
padding: 16px 24px;
551551
font-weight: bold;
552552
font-size: 14px;
@@ -569,7 +569,7 @@
569569

570570
.contributor-row {
571571
display: grid;
572-
grid-template-columns: 0.5fr 0.5fr 2fr 1fr 1fr;
572+
grid-template-columns: 0.5fr 0.5fr 2fr 1fr 1fr 1.5fr;
573573
align-items: center;
574574
padding: 16px 24px;
575575
transition: background-color 0.2s ease;
@@ -615,6 +615,76 @@
615615
padding: 8px;
616616
}
617617

618+
/* Badge display styles */
619+
.contributor-badges {
620+
display: flex;
621+
align-items: center;
622+
gap: 8px;
623+
flex-wrap: wrap;
624+
}
625+
626+
.contributor-badge-icon {
627+
width: 44px;
628+
height: 44px;
629+
object-fit: contain;
630+
border-radius: 8px;
631+
transition: transform 0.2s ease, box-shadow 0.2s ease;
632+
cursor: pointer;
633+
background: rgba(255, 255, 255, 0.1);
634+
padding: 2px;
635+
}
636+
637+
.contributor-badge-icon:hover {
638+
transform: scale(1.15);
639+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
640+
z-index: 10;
641+
position: relative;
642+
}
643+
644+
.username-with-badges {
645+
display: flex;
646+
flex-direction: column;
647+
align-items: flex-start;
648+
gap: 4px;
649+
}
650+
651+
/* Badges column styling */
652+
.contributor-cell.badges-cell {
653+
display: flex;
654+
align-items: center;
655+
justify-content: flex-start;
656+
padding: 8px;
657+
}
658+
659+
/* Badge display in top performer cards */
660+
.top-performer-card .contributor-badges {
661+
margin-top: 8px;
662+
justify-content: center;
663+
}
664+
665+
.top-performer-card .contributor-badge-icon {
666+
width: 36px;
667+
height: 36px;
668+
}
669+
670+
/* Badge display in podium cards */
671+
.podium-card .details {
672+
display: flex;
673+
flex-direction: column;
674+
align-items: center;
675+
gap: 8px;
676+
}
677+
678+
.podium-card .contributor-badges {
679+
margin-top: 4px;
680+
justify-content: center;
681+
}
682+
683+
.podium-card .contributor-badge-icon {
684+
width: 32px;
685+
height: 32px;
686+
}
687+
618688
/* Position the badge inside the cell */
619689
.rank-badge {
620690
position: absolute;

src/components/dashboard/LeaderBoard/leaderboard.tsx

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React, { JSX, useState } from "react";
33
import { motion } from "framer-motion";
44
import { FaStar, FaCode, FaUsers, FaGithub, FaSearch } from "react-icons/fa";
55
import { ChevronRight, ChevronLeft } from "lucide-react";
6-
import { useColorMode } from "@docusaurus/theme-common";
6+
import { useSafeColorMode } from "@site/src/utils/useSafeColorMode";
77
import { useCommunityStatsContext } from "@site/src/lib/statsProvider";
88
import PRListModal from "./PRListModal";
99
import { mockContributors } from "./mockData";
@@ -29,6 +29,7 @@ interface Contributor {
2929
points: number;
3030
prs: number;
3131
prDetails?: PRDetails[];
32+
badges?: string[]; // Array of badge image paths
3233
}
3334

3435
interface Stats {
@@ -37,6 +38,69 @@ interface Stats {
3738
flooredTotalPoints: number;
3839
}
3940

41+
// Badge configuration - maps badge numbers to achievement criteria
42+
const BADGE_CONFIG = [
43+
{ image: "/badges/1.png", name: "First Contribution", criteria: (prs: number) => prs >= 1 },
44+
{ image: "/badges/2.png", name: "Bronze Contributor", criteria: (prs: number) => prs >= 5 },
45+
{ image: "/badges/3.png", name: "Silver Contributor", criteria: (prs: number) => prs >= 10 },
46+
{ image: "/badges/4.png", name: "Gold Contributor", criteria: (prs: number) => prs >= 25 },
47+
{ image: "/badges/5.png", name: "Platinum Contributor", criteria: (prs: number) => prs >= 50 },
48+
{ image: "/badges/6.png", name: "Diamond Contributor", criteria: (prs: number) => prs >= 100 },
49+
{ image: "/badges/7.png", name: "Points Master", criteria: (_: number, points: number) => points >= 500 },
50+
{ image: "/badges/8.png", name: "Elite Contributor", criteria: (prs: number) => prs >= 200 },
51+
{ image: "/badges/9.png", name: "Legendary Contributor", criteria: (prs: number) => prs >= 500 },
52+
{ image: "/badges/10.png", name: "Hall of Fame", criteria: (prs: number, points: number) => prs >= 1000 || points >= 5000 },
53+
];
54+
55+
/**
56+
* Determines which badges a contributor should have based on their stats
57+
*/
58+
function getContributorBadges(contributor: Contributor, rank: number): string[] {
59+
const badges: string[] = [];
60+
61+
// Special rank-based badges
62+
if (rank === 1) {
63+
badges.push("/badges/10.png"); // Hall of Fame for #1
64+
} else if (rank === 2) {
65+
badges.push("/badges/9.png"); // Legendary for #2
66+
} else if (rank === 3) {
67+
badges.push("/badges/8.png"); // Elite for #3
68+
}
69+
70+
// Achievement-based badges
71+
BADGE_CONFIG.forEach((badge) => {
72+
if (badge.criteria(contributor.prs, contributor.points)) {
73+
// Avoid duplicates
74+
if (!badges.includes(badge.image)) {
75+
badges.push(badge.image);
76+
}
77+
}
78+
});
79+
80+
return badges;
81+
}
82+
83+
/**
84+
* Badge display component
85+
*/
86+
function ContributorBadges({ badges }: { badges: string[] }) {
87+
if (!badges || badges.length === 0) return null;
88+
89+
return (
90+
<div className="contributor-badges">
91+
{badges.map((badge, index) => (
92+
<img
93+
key={index}
94+
src={badge}
95+
alt={`Badge ${index + 1}`}
96+
className="contributor-badge-icon"
97+
title={BADGE_CONFIG.find(b => b.image === badge)?.name || "Achievement Badge"}
98+
/>
99+
))}
100+
</div>
101+
);
102+
}
103+
40104
function Badge({
41105
count,
42106
label,
@@ -93,9 +157,9 @@ function TopPerformerCard({
93157
rank: number;
94158
onPRClick: (contributor: Contributor) => void;
95159
}) {
96-
const { colorMode } = useColorMode();
97-
const isDark = colorMode === "dark";
160+
const { isDark } = useSafeColorMode();
98161
const rankClass = rank === 1 ? "top-1" : rank === 2 ? "top-2" : "top-3";
162+
const badges = getContributorBadges(contributor, rank);
99163

100164
return (
101165
<div className={`top-performer-card ${isDark ? "dark" : "light"}`}>
@@ -116,6 +180,7 @@ function TopPerformerCard({
116180
>
117181
{contributor.username}
118182
</a>
183+
<ContributorBadges badges={badges} />
119184
<div className="badges-container">
120185
<Badge
121186
count={contributor.prs}
@@ -146,8 +211,7 @@ export default function LeaderBoard(): JSX.Element {
146211
setTimeFilter,
147212
} = useCommunityStatsContext();
148213

149-
const { colorMode } = useColorMode();
150-
const isDark = colorMode === "dark";
214+
const { isDark } = useSafeColorMode();
151215

152216
const [searchQuery, setSearchQuery] = useState("");
153217
const [currentPage, setCurrentPage] = useState(1);
@@ -377,6 +441,7 @@ export default function LeaderBoard(): JSX.Element {
377441
/>
378442
<div className="details">
379443
<p className="username">{filteredContributors[1].username}</p>
444+
<ContributorBadges badges={getContributorBadges(filteredContributors[1], 2)} />
380445
<div className="stats">
381446
<button
382447
className="prs"
@@ -402,6 +467,7 @@ export default function LeaderBoard(): JSX.Element {
402467
/>
403468
<div className="details">
404469
<p className="username">{filteredContributors[0].username}</p>
470+
<ContributorBadges badges={getContributorBadges(filteredContributors[0], 1)} />
405471
<div className="stats">
406472
<button
407473
className="prs"
@@ -427,6 +493,7 @@ export default function LeaderBoard(): JSX.Element {
427493
/>
428494
<div className="details">
429495
<p className="username">{filteredContributors[2].username}</p>
496+
<ContributorBadges badges={getContributorBadges(filteredContributors[2], 3)} />
430497
<div className="stats">
431498
<button
432499
className="prs"
@@ -574,6 +641,7 @@ export default function LeaderBoard(): JSX.Element {
574641
<div className="contributor-cell username-cell">User</div>
575642
<div className="contributor-cell prs-cell">PRs</div>
576643
<div className="contributor-cell points-cell">Points</div>
644+
<div className="contributor-cell badges-cell">Badges</div>
577645
</div>
578646
{currentItems.map((contributor, index) => (
579647
<motion.div
@@ -629,6 +697,11 @@ export default function LeaderBoard(): JSX.Element {
629697
color={{ background: "#ede9fe", color: "#7c3aed" }}
630698
/>
631699
</div>
700+
<div className="contributor-cell badges-cell">
701+
<ContributorBadges
702+
badges={getContributorBadges(contributor, indexOfFirst + index + 1)}
703+
/>
704+
</div>
632705
</motion.div>
633706
))}
634707

src/components/faqs/faqs.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useState } from "react";
22
import { FiChevronDown } from "react-icons/fi";
33
import { motion } from "framer-motion";
4-
import { useColorMode } from "@docusaurus/theme-common"; // Docusaurus theme detection
4+
import { useSafeColorMode } from "../../utils/useSafeColorMode";
55

66
const faqData = [
77
{
@@ -44,8 +44,7 @@ const faqData = [
4444

4545
const FAQs: React.FC = () => {
4646
const [activeIndex, setActiveIndex] = useState<number | null>(null);
47-
const { colorMode } = useColorMode();
48-
const isDark = colorMode === "dark";
47+
const { colorMode, isDark } = useSafeColorMode();
4948

5049
const toggleAccordion = (index: number) => {
5150
setActiveIndex(activeIndex === index ? null : index);

src/components/ourProjects.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { ChevronRight } from "lucide-react";
44
import { useState } from "react";
55
import { motion } from "framer-motion";
66
import React from "react";
7-
import { useColorMode } from "@docusaurus/theme-common";
7+
import { useSafeColorMode } from "../utils/useSafeColorMode";
88
// Mobile-specific overrides for very small screens (<768px and <320px)
99
import "./ourProjects.mobile.css";
1010
// Import projects data and types
@@ -51,8 +51,7 @@ export interface OurProjectsProps {
5151
const OurProjects: React.FC<OurProjectsProps> = ({
5252
OurProjectsData: legacyData,
5353
}) => {
54-
const { colorMode } = useColorMode(); // light or dark
55-
const isDark = colorMode === "dark";
54+
const { colorMode, isDark } = useSafeColorMode();
5655

5756
// Use JSON data by default, fallback to legacy props for backward compatibility
5857
// Convert legacy data to new format if needed

src/components/testimonials/TestimonialCard.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from "react";
22
import { motion } from "framer-motion";
33
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
4-
import { useColorMode } from "@docusaurus/theme-common";
4+
import { useSafeColorMode } from "../../utils/useSafeColorMode";
55

66
interface TestimonialCardProps {
77
name: string;
@@ -20,8 +20,7 @@ const TestimonialCard: React.FC<TestimonialCardProps> = ({
2020
avatar,
2121
link,
2222
}) => {
23-
const { colorMode } = useColorMode();
24-
const isDark = colorMode === "dark";
23+
const { colorMode, isDark } = useSafeColorMode();
2524

2625
// Function to format the link display
2726
const formatLinkDisplay = (url: string) => {

src/components/topmate/TopMateCard.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from "react";
22
import { motion } from "framer-motion";
33
import { ArrowUpRight, Clock } from "lucide-react";
4-
import { useColorMode } from "@docusaurus/theme-common";
4+
import { useSafeColorMode } from "../../utils/useSafeColorMode";
55

66
interface TopMateCardProps {
77
title: string;
@@ -20,8 +20,7 @@ const TopMateCard: React.FC<TopMateCardProps> = ({
2020
username,
2121
setShowTopmate,
2222
}) => {
23-
const { colorMode } = useColorMode();
24-
const isDark = colorMode === "dark";
23+
const { colorMode, isDark } = useSafeColorMode();
2524

2625
return (
2726
<motion.div

src/components/topmate/TopMateSection.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import React from "react";
22
import TopMateCard from "./TopMateCard";
3-
import { useColorMode } from "@docusaurus/theme-common";
3+
import { useSafeColorMode } from "../../utils/useSafeColorMode";
44

55
const TopMateSection = ({ setShowTopmate }) => {
6-
const { colorMode } = useColorMode(); // Get current theme: 'light' or 'dark'
6+
const { colorMode } = useSafeColorMode(); // Get current theme: 'light' or 'dark'
77

88
const profileData = {
99
title: "1:1 Mentorship Call",

src/theme/ColorModeToggle/index.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,22 @@
11
import React from "react";
2-
import { useColorMode } from "@docusaurus/theme-common";
2+
import { useSafeColorMode } from "@site/src/utils/useSafeColorMode";
3+
import ExecutionEnvironment from "@docusaurus/ExecutionEnvironment";
34

45
export default function ColorModeToggle(): JSX.Element {
5-
const { colorMode, setColorMode } = useColorMode();
6+
const { colorMode } = useSafeColorMode();
7+
8+
// Safe setColorMode that works with DOM
9+
const setColorMode = (mode: "light" | "dark") => {
10+
if (!ExecutionEnvironment.canUseDOM) return;
11+
document.documentElement.setAttribute("data-theme", mode);
12+
// Also trigger Docusaurus's internal theme change if available
13+
try {
14+
const { setColorMode: docusaurusSetColorMode } = require("@docusaurus/theme-common");
15+
docusaurusSetColorMode(mode);
16+
} catch (e) {
17+
// Fallback: just set the DOM attribute
18+
}
19+
};
620

721
const toggleColorMode = () => {
822
const newMode = colorMode === "dark" ? "light" : "dark";

0 commit comments

Comments
 (0)