From 8180ebf4302534009a95a2eb921a4f3be7d05604 Mon Sep 17 00:00:00 2001 From: Edmond Date: Fri, 14 Aug 2026 17:06:24 +0100 Subject: [PATCH] Add PyCon Cameroon 2026 countdown banner Adds a fixed banner above the navbar that counts down to the September 17 event and links to cm.pycon.org. It disappears once the countdown reaches zero and the page layout shifts to avoid any overlap. Co-Authored-By: Claude Sonnet 5 --- src/App.css | 2 + src/components/PyConBanner.tsx | 103 +++++++++++++++++++++++++++++++++ src/layouts/Navbar.tsx | 3 + 3 files changed, 108 insertions(+) create mode 100644 src/components/PyConBanner.tsx diff --git a/src/App.css b/src/App.css index f7c95b5..4db9e8d 100644 --- a/src/App.css +++ b/src/App.css @@ -126,6 +126,8 @@ body { @apply bg-background text-foreground font-space-mono; + padding-top: var(--pycon-banner-height, 0px); + transition: padding-top 0.2s ease; } h1, diff --git a/src/components/PyConBanner.tsx b/src/components/PyConBanner.tsx new file mode 100644 index 0000000..922a55b --- /dev/null +++ b/src/components/PyConBanner.tsx @@ -0,0 +1,103 @@ +import { useEffect, useRef, useState } from "react"; +import { PartyPopper } from "lucide-react"; + +const PYCON_URL = "https://cm.pycon.org"; +const EVENT_START = new Date("2026-09-17T00:00:00+01:00"); + +interface TimeLeft { + days: number; + hours: number; + minutes: number; + seconds: number; +} + +const getTimeLeft = (): TimeLeft => { + const diff = Math.max(0, EVENT_START.getTime() - Date.now()); + return { + days: Math.floor(diff / (1000 * 60 * 60 * 24)), + hours: Math.floor((diff / (1000 * 60 * 60)) % 24), + minutes: Math.floor((diff / (1000 * 60)) % 60), + seconds: Math.floor((diff / 1000) % 60), + }; +}; + +const TimeUnit = ({ value, label }: { value: number; label: string }) => ( +
+ + {String(value).padStart(2, "0")} + + + {label} + +
+); + +export const PyConBanner = () => { + const rootRef = useRef(null); + const [timeLeft, setTimeLeft] = useState(getTimeLeft); + + useEffect(() => { + const interval = setInterval(() => setTimeLeft(getTimeLeft()), 1000); + return () => clearInterval(interval); + }, []); + + const visible = Date.now() < EVENT_START.getTime(); + + useEffect(() => { + const el = rootRef.current; + if (!visible || !el) { + document.documentElement.style.setProperty("--pycon-banner-height", "0px"); + return; + } + + const updateHeight = () => + document.documentElement.style.setProperty( + "--pycon-banner-height", + `${el.offsetHeight}px` + ); + + updateHeight(); + const observer = new ResizeObserver(updateHeight); + observer.observe(el); + + return () => { + observer.disconnect(); + document.documentElement.style.setProperty("--pycon-banner-height", "0px"); + }; + }, [visible]); + + if (!visible) return null; + + return ( +
+ + + + PyCon Cameroon 2026 · Sept 17–19 + + +
+ + : + + : + + : + +
+ + + Learn more → + +
+
+ ); +}; diff --git a/src/layouts/Navbar.tsx b/src/layouts/Navbar.tsx index 87e1c19..4ffdb62 100644 --- a/src/layouts/Navbar.tsx +++ b/src/layouts/Navbar.tsx @@ -18,6 +18,7 @@ import { Menu } from "lucide-react"; import { ModeToggle } from "@/components/mode-toggle"; import { LogoIcon } from "@/components/Icons"; import { LanguageSwitcher } from "@/components/language"; // Import Language Switcher +import { PyConBanner } from "@/components/PyConBanner"; import { motion, AnimatePresence } from "framer-motion"; interface RouteProps { @@ -122,6 +123,8 @@ export const Navbar = () => { animate={{ y: 0 }} transition={{ duration: 0.3 }} > + + {/* Background glow effect */}