Skip to content

Commit 46da450

Browse files
authored
Implement theme toggling with dark and light modes (#3)
* Implement theme toggling feature with dark and light modes; update Navbar and UbuConMap components for theme integration Signed-off-by: Steve Yonkeu <yokwejuste@yahoo.com> * Refactor Navbar component: streamline mobile drawer layout and improve close button positioning Signed-off-by: Steve Yonkeu <yokwejuste@yahoo.com> --------- Signed-off-by: Steve Yonkeu <yokwejuste@yahoo.com>
1 parent f34e090 commit 46da450

7 files changed

Lines changed: 409 additions & 24 deletions

File tree

src/assets/react.svg

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/components/Navbar.jsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import React, { useState, useEffect } from 'react';
22
import { NavLink, Link, useLocation } from 'react-router-dom';
33
import { useTranslation } from 'react-i18next';
4-
import { Languages } from 'lucide-react';
4+
import { Languages, Sun, Moon } from 'lucide-react';
5+
import { useTheme } from '../hooks/useTheme';
56

67
const Navbar = () => {
78
const [isOpen, setIsOpen] = useState(false);
89
const [scrolled, setScrolled] = useState(false);
910
const location = useLocation();
1011
const { t, i18n } = useTranslation();
12+
const { theme, toggleTheme } = useTheme();
1113

1214
const toggleLanguage = () => {
1315
i18n.changeLanguage(i18n.language === 'en' ? 'fr' : 'en');
@@ -61,6 +63,9 @@ const Navbar = () => {
6163
<Languages size={16} />
6264
{i18n.language === 'en' ? 'FR' : 'EN'}
6365
</button>
66+
<button onClick={toggleTheme} className="theme-toggle" aria-label="Toggle theme">
67+
{theme === 'dark' ? <Sun size={18} /> : <Moon size={18} />}
68+
</button>
6469
</div>
6570

6671
{/* Mobile drawer overlay */}
@@ -75,6 +80,12 @@ const Navbar = () => {
7580
>
7681
&times;
7782
</button>
83+
<div className="nav-drawer-header">
84+
<Link to="/" className="nav-logo" onClick={() => setIsOpen(false)}>
85+
<img src="/images/branding/python-cameroon-logo.webp" alt="PyCon Cameroon Logo" />
86+
<span>PyCon CM</span>
87+
</Link>
88+
</div>
7889
<div className="nav-drawer-links">
7990
<NavLink to="/about" className={({ isActive }) => isActive ? "active" : ""}>{t('nav.about')}</NavLink>
8091
<NavLink to="/speakers" className={({ isActive }) => isActive ? "active" : ""}>{t('nav.speakers')}</NavLink>
@@ -85,10 +96,15 @@ const Navbar = () => {
8596
<img src="/images/partners/canonical-cm.webp" alt="" style={{ width: '22px', height: '22px', objectFit: 'contain', borderRadius: '50%' }} />
8697
{t('nav.ubucon')}
8798
</NavLink>
88-
<button onClick={toggleLanguage} className="lang-toggle" aria-label="Toggle language">
89-
<Languages size={16} />
90-
{i18n.language === 'en' ? 'FR' : 'EN'}
91-
</button>
99+
<div className="nav-drawer-actions">
100+
<button onClick={toggleLanguage} className="lang-toggle" aria-label="Toggle language">
101+
<Languages size={18} />
102+
{i18n.language === 'en' ? 'FR' : 'EN'}
103+
</button>
104+
<button onClick={toggleTheme} className="theme-toggle" aria-label="Toggle theme">
105+
{theme === 'dark' ? <Sun size={20} /> : <Moon size={20} />}
106+
</button>
107+
</div>
92108
</div>
93109
</div>
94110

src/components/UbuConMap.jsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,30 @@ const ubuntuIcon = new L.DivIcon({
1313

1414
const CAMEROON_CENTER = [5.9631, 10.1591];
1515

16+
const TILES = {
17+
dark: {
18+
url: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',
19+
attribution: '&copy; <a href="https://carto.com/">CARTO</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
20+
},
21+
light: {
22+
url: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
23+
attribution: '&copy; <a href="https://carto.com/">CARTO</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
24+
},
25+
};
26+
1627
const UbuConMap = () => {
1728
const [events, setEvents] = useState([]);
1829
const [loading, setLoading] = useState(true);
1930
const [error, setError] = useState(null);
31+
const [theme, setTheme] = useState(document.documentElement.getAttribute('data-theme') || 'dark');
32+
33+
useEffect(() => {
34+
const observer = new MutationObserver(() => {
35+
setTheme(document.documentElement.getAttribute('data-theme') || 'dark');
36+
});
37+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
38+
return () => observer.disconnect();
39+
}, []);
2040

2141
useEffect(() => {
2242
fetch('https://ubucon.org/events.json')
@@ -35,6 +55,8 @@ const UbuConMap = () => {
3555
});
3656
}, []);
3757

58+
const tile = TILES[theme] || TILES.dark;
59+
3860
if (loading) {
3961
return (
4062
<div style={{ height: '450px', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--color-dark-alt)', borderRadius: 'var(--radius-md)' }}>
@@ -55,12 +77,13 @@ const UbuConMap = () => {
5577
<MapContainer
5678
center={CAMEROON_CENTER}
5779
zoom={4}
58-
style={{ height: '450px', width: '100%', borderRadius: 'var(--radius-md)' }}
80+
style={{ height: '450px', width: '100%', borderRadius: 'var(--radius-md)', zIndex: 0 }}
5981
scrollWheelZoom={true}
6082
>
6183
<TileLayer
62-
attribution='&copy; <a href="https://carto.com/">CARTO</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
63-
url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"
84+
key={theme}
85+
attribution={tile.attribution}
86+
url={tile.url}
6487
/>
6588
{events.map((evt, i) => (
6689
<Marker key={i} position={evt.coordinates} icon={ubuntuIcon}>

src/hooks/useTheme.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { useState, useEffect, useCallback } from 'react';
2+
3+
function getInitialTheme() {
4+
const saved = localStorage.getItem('pycon-theme');
5+
if (saved === 'light' || saved === 'dark') return saved;
6+
return 'dark';
7+
}
8+
9+
export function useTheme() {
10+
const [theme, setTheme] = useState(getInitialTheme);
11+
12+
useEffect(() => {
13+
document.documentElement.setAttribute('data-theme', theme);
14+
localStorage.setItem('pycon-theme', theme);
15+
}, [theme]);
16+
17+
const toggleTheme = useCallback(() => {
18+
setTheme(prev => prev === 'dark' ? 'light' : 'dark');
19+
}, []);
20+
21+
return { theme, toggleTheme };
22+
}

0 commit comments

Comments
 (0)