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 Cameroon Logo + 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 */} -