diff --git a/src/assets/react.svg b/src/assets/react.svg
deleted file mode 100644
index 6c87de9..0000000
--- a/src/assets/react.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx
index 3d5b096..c13e712 100644
--- a/src/components/Navbar.jsx
+++ b/src/components/Navbar.jsx
@@ -1,13 +1,15 @@
import React, { useState, useEffect } from 'react';
import { NavLink, Link, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
-import { Languages } from 'lucide-react';
+import { Languages, Sun, Moon } from 'lucide-react';
+import { useTheme } from '../hooks/useTheme';
const Navbar = () => {
const [isOpen, setIsOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const location = useLocation();
const { t, i18n } = useTranslation();
+ const { theme, toggleTheme } = useTheme();
const toggleLanguage = () => {
i18n.changeLanguage(i18n.language === 'en' ? 'fr' : 'en');
@@ -61,6 +63,9 @@ const Navbar = () => {
{i18n.language === 'en' ? 'FR' : 'EN'}
+
{/* Mobile drawer overlay */}
@@ -75,6 +80,12 @@ const Navbar = () => {
>
×
+
+
setIsOpen(false)}>
+

+
PyCon CM
+
+
isActive ? "active" : ""}>{t('nav.about')}
isActive ? "active" : ""}>{t('nav.speakers')}
@@ -85,10 +96,15 @@ const Navbar = () => {

{t('nav.ubucon')}
-
+
+
+
+
diff --git a/src/components/UbuConMap.jsx b/src/components/UbuConMap.jsx
index c080057..9864c0a 100644
--- a/src/components/UbuConMap.jsx
+++ b/src/components/UbuConMap.jsx
@@ -13,10 +13,30 @@ const ubuntuIcon = new L.DivIcon({
const CAMEROON_CENTER = [5.9631, 10.1591];
+const TILES = {
+ dark: {
+ url: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',
+ attribution: '© CARTO © OpenStreetMap',
+ },
+ light: {
+ url: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
+ attribution: '© CARTO © OpenStreetMap',
+ },
+};
+
const UbuConMap = () => {
const [events, setEvents] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
+ const [theme, setTheme] = useState(document.documentElement.getAttribute('data-theme') || 'dark');
+
+ useEffect(() => {
+ const observer = new MutationObserver(() => {
+ setTheme(document.documentElement.getAttribute('data-theme') || 'dark');
+ });
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
+ return () => observer.disconnect();
+ }, []);
useEffect(() => {
fetch('https://ubucon.org/events.json')
@@ -35,6 +55,8 @@ const UbuConMap = () => {
});
}, []);
+ const tile = TILES[theme] || TILES.dark;
+
if (loading) {
return (
@@ -55,12 +77,13 @@ const UbuConMap = () => {
{events.map((evt, i) => (
diff --git a/src/hooks/useTheme.js b/src/hooks/useTheme.js
new file mode 100644
index 0000000..ed44023
--- /dev/null
+++ b/src/hooks/useTheme.js
@@ -0,0 +1,22 @@
+import { useState, useEffect, useCallback } from 'react';
+
+function getInitialTheme() {
+ const saved = localStorage.getItem('pycon-theme');
+ if (saved === 'light' || saved === 'dark') return saved;
+ return 'dark';
+}
+
+export function useTheme() {
+ const [theme, setTheme] = useState(getInitialTheme);
+
+ useEffect(() => {
+ document.documentElement.setAttribute('data-theme', theme);
+ localStorage.setItem('pycon-theme', theme);
+ }, [theme]);
+
+ const toggleTheme = useCallback(() => {
+ setTheme(prev => prev === 'dark' ? 'light' : 'dark');
+ }, []);
+
+ return { theme, toggleTheme };
+}
diff --git a/src/index.css b/src/index.css
index d434a84..c24e753 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,8 +1,10 @@
:root {
- /* Primary Colors */
+ /* Primary Colors (Dark theme — default) */
--color-black: #0d0d0d;
--color-dark: #1a1a1a;
--color-dark-alt: #242424;
+ --color-border: rgba(224, 122, 36, 0.15);
+ --color-surface: #1a1a1a;
/* Background Images */
--bg-tribal: url('/images/patterns/97568bbec02a103e305ee0d2bbaa6106.webp');
@@ -11,7 +13,7 @@
--bg-drums: url('/images/patterns/African.webp');
--bg-silver: url('/images/patterns/ce62691b36daffc632c28793c5a28ba3.webp');
- /* Accent Colors*/
+ /* Accent Colors */
--color-orange: #e07a24;
--color-orange-bright: #f59e0b;
--color-yellow: #f5c518;
@@ -68,6 +70,289 @@
--font-display: var(--font-tribal);
}
+/* ===================================
+ Light Theme Override
+ =================================== */
+[data-theme="light"] {
+ --color-black: #ffffff;
+ --color-dark: #f5f5f5;
+ --color-dark-alt: #e8e8e8;
+ --color-border: rgba(0, 0, 0, 0.1);
+ --color-surface: #ffffff;
+
+ --color-text-primary: #1a1a1a;
+ --color-text-secondary: rgba(0, 0, 0, 0.7);
+ --color-text-muted: rgba(0, 0, 0, 0.5);
+
+ --gradient-dark: none;
+
+ --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.08);
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1);
+ --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12);
+ --shadow-glow: 0 0 20px rgba(224, 122, 36, 0.15);
+}
+
+[data-theme="light"] body {
+ background: #ffffff;
+ color: #1a1a1a;
+}
+
+[data-theme="light"] .navbar {
+ background: rgba(255, 255, 255, 0.95);
+ backdrop-filter: blur(10px);
+}
+
+[data-theme="light"] .navbar.scrolled {
+ background: rgba(255, 255, 255, 0.98);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+
+[data-theme="light"] .card {
+ background: #ffffff;
+ border: 1px solid rgba(0, 0, 0, 0.08);
+}
+
+[data-theme="light"] .card:hover {
+ border-color: rgba(224, 122, 36, 0.3);
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
+}
+
+[data-theme="light"] .card.bg-dark {
+ background: #f5f5f5;
+}
+
+[data-theme="light"] .bg-dark {
+ background: #f5f5f5;
+}
+
+[data-theme="light"] .hero-background {
+ opacity: 0.15;
+ filter: blur(2px) saturate(0.6);
+}
+
+[data-theme="light"] .hero-overlay {
+ background: rgba(245, 245, 245, 0.75) !important;
+}
+
+[data-theme="light"] .hero-title,
+[data-theme="light"] .hero-subtitle,
+[data-theme="light"] .hero-date {
+ color: #1a1a1a;
+}
+
+[data-theme="light"] .hero-title .highlight {
+ color: var(--color-orange);
+}
+
+[data-theme="light"] .footer {
+ background-color: #1a1a1a !important;
+ background-image: var(--bg-tribal) !important;
+ background-size: cover !important;
+ background-blend-mode: overlay !important;
+ color: #ffffff;
+}
+
+[data-theme="light"] .footer::before {
+ background: radial-gradient(circle at center, transparent, #0d0d0d) !important;
+}
+
+[data-theme="light"] .footer .footer-description,
+[data-theme="light"] .footer .footer-links a,
+[data-theme="light"] .footer .footer-bottom,
+[data-theme="light"] .footer .footer-bottom a,
+[data-theme="light"] .footer p {
+ color: rgba(255, 255, 255, 0.8);
+}
+
+[data-theme="light"] .footer .footer-title {
+ color: #ffffff;
+}
+
+[data-theme="light"] .footer .social-link {
+ color: rgba(255, 255, 255, 0.8);
+}
+
+[data-theme="light"] .footer .tribal-border {
+ filter: none;
+ opacity: 0.6;
+}
+
+[data-theme="light"] .page-header:not(.page-header-dark) {
+ background: #f0f0f0 !important;
+ color: #1a1a1a;
+}
+
+[data-theme="light"] .page-header:not(.page-header-dark) h1,
+[data-theme="light"] .page-header:not(.page-header-dark) p {
+ color: #1a1a1a;
+ text-shadow: none;
+}
+
+[data-theme="light"] .page-header-dark h1,
+[data-theme="light"] .page-header-dark p {
+ color: white;
+}
+
+[data-theme="light"] .page-header .text-gradient {
+ background: var(--gradient-primary);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+[data-theme="light"] .nav-links a {
+ color: rgba(0, 0, 0, 0.65);
+}
+
+[data-theme="light"] .nav-links a:hover,
+[data-theme="light"] .nav-links a.active {
+ color: var(--color-orange);
+ background: rgba(224, 122, 36, 0.08);
+}
+
+[data-theme="light"] .nav-logo span {
+ color: #1a1a1a;
+}
+
+[data-theme="light"] .alert {
+ border-color: rgba(0, 0, 0, 0.1);
+}
+
+[data-theme="light"] .alert-info {
+ background: rgba(59, 130, 246, 0.06);
+ border-left-color: var(--color-blue);
+}
+
+[data-theme="light"] .alert-warning {
+ background: rgba(245, 158, 11, 0.06);
+ border-left-color: var(--color-orange-bright);
+}
+
+[data-theme="light"] .alert-success {
+ background: rgba(45, 143, 78, 0.06);
+ border-left-color: var(--color-green);
+}
+
+[data-theme="light"] .alert-danger {
+ background: rgba(201, 48, 44, 0.06);
+ border-left-color: var(--color-red);
+}
+
+[data-theme="light"] .btn-secondary {
+ border-color: var(--color-orange);
+ color: var(--color-orange);
+ background: transparent;
+}
+
+[data-theme="light"] .btn-secondary:hover {
+ background: rgba(224, 122, 36, 0.08);
+}
+
+[data-theme="light"] .accordion-header {
+ background: #f0f0f0;
+ color: #1a1a1a;
+}
+
+[data-theme="light"] .accordion-item.active .accordion-header {
+ background: rgba(224, 122, 36, 0.08);
+}
+
+[data-theme="light"] .sponsor-card {
+ background: #ffffff;
+ border: 1px solid rgba(0, 0, 0, 0.08);
+}
+
+[data-theme="light"] .timeline-item {
+ border-left-color: rgba(0, 0, 0, 0.15);
+}
+
+[data-theme="light"] .nav-drawer {
+ background: #ffffff;
+}
+
+[data-theme="light"] .nav-drawer-links a {
+ color: rgba(0, 0, 0, 0.65);
+}
+
+[data-theme="light"] .nav-drawer-links a:hover,
+[data-theme="light"] .nav-drawer-links a.active {
+ color: var(--color-orange);
+ background: rgba(224, 122, 36, 0.08);
+}
+
+[data-theme="light"] .nav-drawer-close {
+ color: #1a1a1a;
+}
+
+[data-theme="light"] .stat-label {
+ color: rgba(0, 0, 0, 0.6);
+}
+
+[data-theme="light"] .tribal-border {
+ filter: none;
+ opacity: 1;
+ background-color: #1a1a1a;
+ background-blend-mode: normal;
+}
+
+[data-theme="light"] .navbar .tribal-border {
+ filter: none;
+ opacity: 1;
+ background-color: #1a1a1a;
+ background-blend-mode: normal;
+}
+
+[data-theme="light"] .lang-toggle {
+ border-color: var(--color-orange);
+ color: var(--color-orange);
+}
+
+[data-theme="light"] .theme-toggle {
+ border-color: rgba(0, 0, 0, 0.2);
+ color: #1a1a1a;
+}
+
+[data-theme="light"] .theme-toggle:hover {
+ background: rgba(0, 0, 0, 0.05);
+ border-color: rgba(0, 0, 0, 0.3);
+}
+
+/* Leaflet z-index containment */
+.leaflet-container {
+ z-index: 0 !important;
+}
+
+.leaflet-pane,
+.leaflet-control {
+ z-index: auto !important;
+}
+
+.leaflet-top,
+.leaflet-bottom {
+ z-index: 1 !important;
+}
+
+/* Theme toggle button */
+.theme-toggle {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: transparent;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ color: var(--color-text-secondary);
+ padding: 4px 8px;
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ font-size: 1rem;
+ transition: background 0.2s, color 0.2s, border-color 0.2s;
+ line-height: 1;
+}
+
+.theme-toggle:hover {
+ background: rgba(255, 255, 255, 0.1);
+ border-color: rgba(255, 255, 255, 0.3);
+}
+
/* Typography Utilities */
.font-tribal {
font-family: var(--font-tribal);
@@ -447,7 +732,7 @@ p {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
- z-index: 999;
+ z-index: 9998;
backdrop-filter: blur(4px);
}
@@ -460,10 +745,11 @@ p {
width: 100%;
height: 100dvh;
background: var(--color-dark);
- z-index: 1000;
+ z-index: 9999;
transform: translateX(100%);
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
- padding: var(--spacing-xl) var(--spacing-lg);
+ padding: var(--spacing-sm) var(--spacing-lg) var(--spacing-md);
+ overflow: hidden;
}
.nav-drawer.open {
@@ -472,8 +758,8 @@ p {
.nav-drawer-close {
position: absolute;
- top: var(--spacing-md);
- right: var(--spacing-md);
+ top: var(--spacing-sm);
+ right: var(--spacing-lg);
background: none;
border: none;
color: var(--color-text);
@@ -491,8 +777,9 @@ p {
.nav-drawer-links {
display: flex;
flex-direction: column;
- gap: var(--spacing-sm);
- margin-top: var(--spacing-xl);
+ gap: var(--spacing-xs);
+ margin-top: var(--spacing-sm);
+ flex: 1;
}
.nav-drawer-links a {
@@ -511,11 +798,45 @@ p {
background: rgba(224, 122, 36, 0.1);
}
- .nav-drawer-links .lang-toggle {
- margin-top: var(--spacing-sm);
- align-self: flex-start;
+ .nav-drawer-header {
+ display: flex;
+ align-items: center;
+ padding: 0;
+ margin-top: var(--spacing-md);
+ margin-bottom: 0;
+ }
+
+ .nav-drawer-header .nav-logo {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .nav-drawer-header .nav-logo img {
+ width: 36px;
+ height: 36px;
+ }
+
+ .nav-drawer-header .nav-logo span {
+ font-family: var(--font-tribal);
+ font-size: 1.1rem;
+ color: var(--color-orange);
+ }
+
+ .nav-drawer-actions {
+ display: flex;
+ gap: var(--spacing-sm);
+ margin-top: auto;
+ padding-top: var(--spacing-sm);
+ border-top: 1px solid var(--color-border);
+ }
+
+ .nav-drawer-actions .lang-toggle,
+ .nav-drawer-actions .theme-toggle {
+ flex: 1;
+ justify-content: center;
font-size: 1rem;
- padding: 6px 14px;
+ padding: 10px 14px;
}
}
@@ -583,7 +904,7 @@ p {
.btn-primary {
background: var(--color-orange);
- color: var(--color-black);
+ color: #0d0d0d;
border-color: var(--color-orange-bright);
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.5);
/* Primitive shadow */
diff --git a/src/main.jsx b/src/main.jsx
index d2a93a5..178a6fc 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -5,6 +5,10 @@ import './index.css'
import './i18n/LanguageContext'
import App from './App.jsx'
+// Set initial theme before render to avoid flash
+const savedTheme = localStorage.getItem('pycon-theme') || 'dark';
+document.documentElement.setAttribute('data-theme', savedTheme);
+
createRoot(document.getElementById('root')).render(
diff --git a/src/pages/UbuCon.jsx b/src/pages/UbuCon.jsx
index 5897edc..16fd942 100644
--- a/src/pages/UbuCon.jsx
+++ b/src/pages/UbuCon.jsx
@@ -18,7 +18,7 @@ const UbuCon = () => {
return (
<>
{/* Page Header */}
-