From 8180ebf4302534009a95a2eb921a4f3be7d05604 Mon Sep 17 00:00:00 2001 From: Edmond Date: Fri, 14 Aug 2026 17:06:24 +0100 Subject: [PATCH 01/17] 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 */}
Date: Fri, 14 Aug 2026 17:53:46 +0100 Subject: [PATCH 02/17] Fix invalid comma syntax in HSL CSS variables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --primary, --secondary, --muted, and --accent used comma-separated HSL values (e.g. 166, 95%, 29%) instead of the space-separated shadcn convention. Tailwind's hsl(var(--x) / ) syntax requires the space form, so with commas the resulting hsl(166, 95%, 29% / .1) was invalid CSS and silently dropped — breaking every bg-primary/xx, border-primary/xx, and gradient utility across the site. --- src/App.css | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/App.css b/src/App.css index 4db9e8d..c2b5075 100644 --- a/src/App.css +++ b/src/App.css @@ -76,13 +76,13 @@ --card-foreground: 240 10% 3.9%; --popover: 0 0% 100%; --popover-foreground: 240 10% 3.9%; - --primary: 166, 95%, 29%; + --primary: 166 95% 29%; --primary-foreground: 355.7 100% 97.3%; - --secondary: 50, 96%, 59%; + --secondary: 50 96% 59%; --secondary-foreground: 240 5.9% 10%; - --muted: 50, 96%, 59%; + --muted: 50 96% 59%; --muted-foreground: 240 3.8% 46.1%; - --accent: 50, 96%, 59%; + --accent: 50 96% 59%; --accent-foreground: 240 5.9% 10%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 0 0% 98%; @@ -103,9 +103,9 @@ --card-foreground: 0 0% 95%; --popover: 0 0% 9%; --popover-foreground: 0 0% 95%; - --primary: 166, 95%, 29%; + --primary: 166 95% 29%; --primary-foreground: 144.9 80.4% 10%; - --secondary: 50, 96%, 59%; + --secondary: 50 96% 59%; --secondary-foreground: 0 0% 98%; --muted: 0 0% 15%; --muted-foreground: 240 5% 64.9%; From 770ab5eb995ba2fdf50624f971d8a53190c0e0a0 Mon Sep 17 00:00:00 2001 From: Edmond Date: Fri, 14 Aug 2026 18:00:51 +0100 Subject: [PATCH 03/17] Fix B2: convert --primary-rgb/--secondary-rgb from HSL to RGB The CSS variables held HSL triples (166, 95%, 29% / 50, 96%, 59%), which is invalid inside rgba(...), silently killing every inline glow/box-shadow/gradient built from them (Hero, Team, FAQ, Sponsors, Newsletter, About, Applications, Statistics, HowItWorks, Services). Recomputed the precise RGB equivalents and fixed Statistics.tsx's stray hsl(var(--primary-rgb)) usage to match the rgba(...) pattern used everywhere else. Co-Authored-By: Claude Sonnet 5 --- src/components/Statistics.tsx | 6 +++--- src/index.css | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/Statistics.tsx b/src/components/Statistics.tsx index 6c7e2b8..5ce746a 100644 --- a/src/components/Statistics.tsx +++ b/src/components/Statistics.tsx @@ -114,9 +114,9 @@ export const Statistics = () => { animate={{ opacity: counterInView ? [0.1, 0.3, 0.1] : 0, background: [ - "radial-gradient(circle, hsl(var(--primary-rgb)) 0%, transparent 70%)", - "radial-gradient(circle, hsl(var(--primary-rgb)) 0%, transparent 80%)", - "radial-gradient(circle, hsl(var(--primary-rgb)) 0%, transparent 70%)" + "radial-gradient(circle, rgba(var(--primary-rgb), 1) 0%, transparent 70%)", + "radial-gradient(circle, rgba(var(--primary-rgb), 1) 0%, transparent 80%)", + "radial-gradient(circle, rgba(var(--primary-rgb), 1) 0%, transparent 70%)" ] }} transition={{ diff --git a/src/index.css b/src/index.css index ebe669e..810a81d 100644 --- a/src/index.css +++ b/src/index.css @@ -1,12 +1,12 @@ :root { - --primary-rgb: 166, 95%, 29%; - --secondary-rgb: 50, 96%, 59%; + --primary-rgb: 4, 144, 111; + --secondary-rgb: 251, 217, 50; /* For green, adjust based on your theme */ } .dark { - --primary-rgb: 166, 95%, 29%; - --secondary-rgb: 50, 96%, 59%; + --primary-rgb: 4, 144, 111; + --secondary-rgb: 251, 217, 50; /* Adjust for dark mode */ } From fadc55b966831ed68f64de64116df7f8f5d630c8 Mon Sep 17 00:00:00 2001 From: Edmond Date: Fri, 14 Aug 2026 18:31:01 +0100 Subject: [PATCH 04/17] Fix B3: hide fake newsletter form, fix dead Hero CTAs Newsletter form promised subscriptions that never actually sent anywhere (setTimeout + console.log). Hide the section until a real backend is decided, and remove its now-dead nav link. Hero CTAs also pointed at broken/removed anchors: - "Explore Python" (#About) now links to python.org - Secondary CTA (#newsletter) is now a "Contact Us" mailto link to organizers@pythoncameroon.org --- src/App.tsx | 3 +- src/containers/Hero.tsx | 8 ++-- src/containers/Newsletter.tsx | 74 ++++++++++++++--------------------- src/layouts/Navbar.tsx | 4 -- 4 files changed, 36 insertions(+), 53 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 80ce3ad..67b09d9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,7 +4,6 @@ import {Footer} from "@/layouts/Footer"; import {Hero} from "@/containers/Hero"; import {HowItWorks} from "@/containers/HowItWorks"; import {Navbar} from "@/layouts/Navbar"; -import {Newsletter} from "@/containers/Newsletter"; import {ScrollToTop} from "@/components/ScrollToTop"; import {Services} from "@/containers/Services"; import {Sponsors} from "@/containers/Sponsors"; @@ -24,7 +23,7 @@ function App() { - + {/* Newsletter masquée : pas de backend réel derrière le formulaire, voir AUDIT.md B3 */}
- - - -
@@ -320,14 +311,6 @@ export const Navbar = () => { - - - - Date: Fri, 14 Aug 2026 23:45:05 +0100 Subject: [PATCH 07/17] Add YouTube link back to footer Co-Authored-By: Claude Sonnet 5 --- src/layouts/Footer.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/layouts/Footer.tsx b/src/layouts/Footer.tsx index f8cf454..da9103d 100644 --- a/src/layouts/Footer.tsx +++ b/src/layouts/Footer.tsx @@ -4,6 +4,7 @@ import { Github, Twitter, Linkedin, + Youtube, MessageCircle, Phone, Heart, @@ -18,6 +19,7 @@ export const Footer = () => { Github: Github, Twitter: Twitter, Linkedin: Linkedin, + Youtube: Youtube, Discord: MessageCircle, WhatsApp: Phone, }; @@ -41,6 +43,11 @@ export const Footer = () => { url: "https://linkedin.com/company/PythonCameroon", icon: "Linkedin", }, + { + name: "Youtube", + url: "https://www.youtube.com/@PythonCameroon", + icon: "Youtube", + }, ], }, { From a8b3d539882bdefabe06f5d72d8172bed8316b67 Mon Sep 17 00:00:00 2001 From: EdGhi Date: Fri, 14 Aug 2026 23:46:59 +0100 Subject: [PATCH 08/17] Fix B6: remove invalid nested in - + - + {isSuccess && ( - { className="text-center mt-4 text-primary" > Opening our Discord in a new tab — join to get updates! - + )} - {["Updates", "Events", "Tutorials", "Community"].map((tag, i) => ( - { }} > {tag} - + ))} - + - - + ); }; diff --git a/src/containers/Services.tsx b/src/containers/Services.tsx index 4a75427..8c982d3 100644 --- a/src/containers/Services.tsx +++ b/src/containers/Services.tsx @@ -2,7 +2,7 @@ import { useRef } from "react"; import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { ChartIcon, WalletIcon, MagnifierIcon } from "@/components/Icons"; import cubeLeg from "../assets/cube-leg.png"; -import { motion, useInView, AnimatePresence } from "framer-motion"; +import { m, useInView, AnimatePresence } from "framer-motion"; interface ServiceProps { title: string; @@ -15,13 +15,13 @@ const serviceList: ServiceProps[] = [ title: "Code Collaboration", description: "Enhance team productivity with Python-based tools for version control, CI/CD, and collaborative coding.", - icon: , + icon: , }, { title: "Project Management", description: "Streamline workflows with Python-powered task automation, reporting, and data visualization.", - icon: , + icon: , }, { title: "Task Automation", @@ -33,10 +33,10 @@ const serviceList: ServiceProps[] = [ export const Services = () => { const sectionRef = useRef(null); - const isInView = useInView(sectionRef, { once: false, amount: 0.2 }); - + const isInView = useInView(sectionRef, { once: true, amount: 0.2 }); + return ( - { exit={{ opacity: 0 }} >
- -
- - - - - + Client-Centric - - {" "} + {" "} Services - + - Empowering teams with Python-driven efficiency, automation, and collaboration. - est invalide dans un

*/} + - +

{serviceList.map(({ icon, title, description }: ServiceProps, index) => ( - - - + - - - +
{icon} - - - +
+ +
- {title} - - - - + + { {description} - +
- -
-
+ ))}
-
+ - { whileHover={{ scale: 1.03 }} style={{ transformStyle: "preserve-3d" }} > - - - - {[...Array(5)].map((_, i) => ( - - ))} - - - - -
+ + + ); }; diff --git a/src/containers/Sponsors.tsx b/src/containers/Sponsors.tsx index 2511a29..02d372b 100644 --- a/src/containers/Sponsors.tsx +++ b/src/containers/Sponsors.tsx @@ -1,8 +1,8 @@ import { useState, useRef } from "react"; -import { motion, useInView, AnimatePresence } from "framer-motion"; +import { m, useInView, AnimatePresence } from "framer-motion"; interface SponsorProps { - icon: string; + icon: string; name: string; link: string; } @@ -14,7 +14,7 @@ const sponsors: SponsorProps[] = [ link: "https://github.com/djangocameroon", }, { - icon: "https://avatars.githubusercontent.com/u/183505611?s=200&v=4", + icon: "https://avatars.githubusercontent.com/u/183505611?s=200&v=4", name: "Angular Cameroon", link: "https://github.com/ngcameroon", }, @@ -22,11 +22,11 @@ const sponsors: SponsorProps[] = [ export const Sponsors = () => { const sectionRef = useRef(null); - const isInView = useInView(sectionRef, { once: false, amount: 0.2 }); + const isInView = useInView(sectionRef, { once: true, amount: 0.2 }); const [hoveredIndex, setHoveredIndex] = useState(null); return ( - { animate={{ opacity: 1 }} exit={{ opacity: 0 }} > - {/* Animated background elements */} + {/* Background elements */}
{/* Content section */} @@ -361,44 +333,24 @@ export const Applications = () => { - {/* Expandable description overlay */} - - {hovered === index && ( - - +

+ {description} +

+ +
+ {techStack.map((tech) => ( + -

{title}

-

- {description} -

- - {/* All tech stack */} -
- {techStack.map((tech, techIndex) => ( - - {tech} - - ))} -
- - - )} - + {tech} +
+ ))} +
+
))} diff --git a/src/containers/FAQ.tsx b/src/containers/FAQ.tsx index fac4909..003ef72 100644 --- a/src/containers/FAQ.tsx +++ b/src/containers/FAQ.tsx @@ -1,6 +1,12 @@ import { useState, useRef } from "react"; -import { m, useInView, AnimatePresence } from "framer-motion"; -import { ChevronDown, MessageCircle, HelpCircle, Sparkles } from "lucide-react"; +import { m, useInView } from "framer-motion"; +import { MessageCircle, HelpCircle, Sparkles } from "lucide-react"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; interface FAQProps { question: string; @@ -42,19 +48,10 @@ const FAQList: FAQProps[] = [ ]; export const FAQ = () => { - const [openItems, setOpenItems] = useState([]); const [hoveredItem, setHoveredItem] = useState(null); const sectionRef = useRef(null); const isInView = useInView(sectionRef, { once: true, amount: 0.2 }); - const toggleItem = (value: string) => { - setOpenItems(prev => - prev.includes(value) - ? prev.filter(item => item !== value) - : [...prev, value] - ); - }; - return ( {
- {/* FAQ Accordion */} - - {FAQList.map(({ question, answer, value }: FAQProps, index) => { - const isOpen = openItems.includes(value); - const isHovered = hoveredItem === value; - - return ( - setHoveredItem(value)} - onMouseLeave={() => setHoveredItem(null)} - > - {/* Animated border gradient */} - + + {FAQList.map(({ question, answer, value }: FAQProps, index) => { + const isHovered = hoveredItem === value; + return ( setHoveredItem(value)} + onMouseLeave={() => setHoveredItem(null)} > - {/* Question header */} - toggleItem(value)} - whileTap={{ scale: 0.98 }} + {/* Animated border gradient */} + + + -
+ + + + + + + + + {question} + + + + + +
+ {answer} +
+
+
+ + {/* Subtle glow effect */} + {isHovered && ( - - - - - - - {question} - -
- - - - -
- - {/* Answer content */} - - {isOpen && ( - - -
- {answer} -
-
-
+ /> )} -
- - {/* Subtle glow effect */} - {isHovered && ( - - )} +
-
- ); - })} + ); + })} +
{/* Contact section */} diff --git a/src/containers/Hero.tsx b/src/containers/Hero.tsx index 238a7bb..9dfb150 100644 --- a/src/containers/Hero.tsx +++ b/src/containers/Hero.tsx @@ -227,27 +227,30 @@ export const Hero = () => { initial="hidden" animate="visible" > -

- - - - {" "} - - is - -

{" "} -

- - - - -

+ {/* Un seul h1 pour la page — les deux lignes restent visuellement séparées (AUDIT.md A5) */} +

+ + + + + {" "} + + is + + + + + + + + +

{ transition={{ duration: 0.6, delay: 0.1 }} className="relative" > -

+

Join Our Daily{" "} @@ -74,7 +74,7 @@ export const Newsletter = () => { }} /> -

+ { target="_blank" rel="noopener noreferrer" className="flex flex-col items-center gap-4 relative" + // Le focus clavier déclenche les mêmes révélations que le hover (AUDIT.md A1) + onFocus={() => setHoveredIndex(index)} + onBlur={() => setHoveredIndex(null)} > {/* Logo container with effects */} { + Open menu - - Menu Icon - +
- - - {/* Grid pattern overlay */}
- + {/* Section heading */} - - - Partnering Organisations - - + + {/* Glowing effect behind text */} - - - + + {/* Animated divider */} - - - {/* Subtitle with animated underline */} - Our amazing partners who help make the Python Cameroon community thrive and grow. - - - + + {/* Sponsors gallery */} - {/* Horizontal connecting line */} - { background: "linear-gradient(to right, transparent, rgba(var(--primary-rgb), 0.3), transparent)" }} /> - + {sponsors.map(({ icon, name, link }: SponsorProps, index) => ( - { onMouseEnter={() => setHoveredIndex(index)} onMouseLeave={() => setHoveredIndex(null)} > -
{/* Logo container with effects */} - {/* Rotating background gradient */} - - + {/* Logo image */} - - { rotate: { duration: 0.5, ease: "easeInOut" } }} /> - - - {/* Pulsing glow effect on hover */} + + + {/* Glow effect on hover */} {hoveredIndex === index && ( - )} - + {/* Expanding ring effect on hover */} {hoveredIndex === index && ( - )} - - + + {/* Sponsor name with underline animation */}
- {name} - - + + {/* Animated underline */} -
- + {/* Visit text that appears on hover */} {hoveredIndex === index && ( - { transition={{ duration: 0.2 }} > Visit - - - + + )}
- + ))} - - + + {/* Call to action button */} - - - - + Become a Partner - + {/* Bottom border animation */} - - + {/* Subtle glow effect */} - - - + + {/* Bottom divider line */} - { background: "linear-gradient(to right, transparent, rgba(var(--primary-rgb), 0.3), transparent)" }} /> - + ); }; diff --git a/src/containers/Team.tsx b/src/containers/Team.tsx index 6921be5..be3fdb3 100644 --- a/src/containers/Team.tsx +++ b/src/containers/Team.tsx @@ -1,9 +1,8 @@ import { useState, useRef } from "react"; -import { motion, useInView, AnimatePresence } from "framer-motion"; +import { m, useInView } from "framer-motion"; import { Users, Star, - Sparkles, User, Globe, Linkedin, @@ -20,7 +19,7 @@ const ProfileImage = ({ isHovered: boolean; }) => { return ( - {/* Profile Image */} {member.image ? ( - ) : ( // Default placeholder when all images fail - - + )} {/* Overlay effect on hover */} - - + ); }; export const Team = () => { const [hoveredMember, setHoveredMember] = useState(null); const sectionRef = useRef(null); - const isInView = useInView(sectionRef, { once: false, amount: 0.2 }); + const isInView = useInView(sectionRef, { once: true, amount: 0.2 }); return ( - { > {/* Animated background elements */}
- - {/* Grid pattern overlay */}
- {/* Floating particles */} - - {isInView && - Array.from({ length: 10 }).map((_, i) => ( - - ))} -
{/* Section header */} - - { Our Amazing Team - + - - +

+ Meet the Python Cameroon Team {/* Glowing effect behind text */} - - - + +

{/* Animated divider */} - - { > Dedicated innovators advancing Python development in Cameroon through collaboration and expertise. - - + + {/* Team grid */} - { const isHovered = hoveredMember === member.name; return ( - { onMouseLeave={() => setHoveredMember(null)} > {/* Animated border gradient */} - - { > {" "} {/* Profile image container */} - {/* Rotating background gradient */} - { {/* Use the ProfileImage component with fallback system */} - {/* Pulsing ring effect */} - {isHovered && ( - - )} - {" "} + {" "} {/* Member info */} - - { transition={{ duration: 0.3 }} > {member.name} - + - {member.role} - - + + {/* Social links */} - {member.links.linkedIn && ( - { }} > - + )} {member.links.website && ( - { }} > - + )} - + {/* Subtle glow effect on hover */} {isHovered && ( - { transition={{ duration: 0.3 }} /> )} - {/* Sparkle effects on hover */} - - {isHovered && ( - <> - {Array.from({ length: 3 }).map((_, i) => ( - - - - ))} - - )} - - - + + ); })} - - + + ); }; diff --git a/src/index.css b/src/index.css index 810a81d..f5e1328 100644 --- a/src/index.css +++ b/src/index.css @@ -33,6 +33,22 @@ html { scroll-behavior: smooth; } +/* AUDIT.md P4 : respecter prefers-reduced-motion pour les animations CSS + (framer-motion est couvert par dans main.tsx) */ +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + /* HeroCards background shadow */ .shadow { position: absolute; diff --git a/src/layouts/Footer.tsx b/src/layouts/Footer.tsx index da9103d..ad78889 100644 --- a/src/layouts/Footer.tsx +++ b/src/layouts/Footer.tsx @@ -1,5 +1,5 @@ import { useState, useRef } from "react"; -import { motion, useInView, AnimatePresence } from "framer-motion"; +import { m, useInView, AnimatePresence } from "framer-motion"; import { Github, Twitter, @@ -12,7 +12,7 @@ import { export const Footer = () => { const footerRef = useRef(null); - const isInView = useInView(footerRef, { once: false, amount: 0.2 }); + const isInView = useInView(footerRef, { once: true, amount: 0.2 }); const [hoveredSection, setHoveredSection] = useState(null); const socialIcons = { @@ -75,7 +75,7 @@ export const Footer = () => { ]; return ( - { animate={{ opacity: 1 }} exit={{ opacity: 0 }} > - {/* Animated background */} + {/* Background */}
- - {/* Grid pattern overlay */} @@ -119,7 +100,7 @@ export const Footer = () => {
{/* Animated top divider */} - {
{/* Logo section with enhanced animations */} - { } transition={{ duration: 0.8, delay: 0.2 }} > - - - {/* Floating particles around logo */} - - {isInView && ( - <> - {Array.from({ length: 6 }).map((_, i) => ( - - ))} - - )} - - {/* Glow effect */} - - + - Python Cameroon - - + + {/* Description */} - { > Empowering Python developers across Cameroon through community, learning, and innovation. - - + + {/* Footer sections with staggered animations */} {footerSections.map((section, sectionIndex) => ( - { onMouseLeave={() => setHoveredSection(null)} > {/* Section title with glow effect */} - { {section.title} {/* Underline animation */} - { {/* Background glow */} {hoveredSection === section.title && ( - { style={{ filter: "blur(10px)" }} /> )} - + {/* Links with hover animations */} @@ -281,7 +216,7 @@ export const Footer = () => { : null; return ( - { duration: 0.4, }} > - { transition={{ duration: 0.2 }} > {IconComponent && ( - @@ -309,73 +244,56 @@ export const Footer = () => { size={16} className="text-primary/70 group-hover:text-primary" /> - + )} {link.name} {/* Underline effect */} - {/* Hover glow effect */} - - - + + ); })} - + ))}
{/* Bottom section with animated copyright */} - - +

© {new Date().getFullYear()}{" "} Python Cameroon . All rights reserved. Built with{" "} - + - {" "} + {" "} for the community. - - +

+
{/* Bottom animated border */} - { "linear-gradient(to right, transparent, rgba(var(--primary-rgb), 0.5), transparent)", }} /> -
+ ); }; diff --git a/src/layouts/Navbar.tsx b/src/layouts/Navbar.tsx index 6c9954c..a47a2a6 100644 --- a/src/layouts/Navbar.tsx +++ b/src/layouts/Navbar.tsx @@ -18,7 +18,7 @@ import { Menu } from "lucide-react"; import { ModeToggle } from "@/components/mode-toggle"; import { LogoIcon } from "@/components/Icons"; import { PyConBanner } from "@/components/PyConBanner"; -import { motion, AnimatePresence } from "framer-motion"; +import { m, AnimatePresence } from "framer-motion"; interface RouteProps { href: string; @@ -36,38 +36,6 @@ const routeList: RouteProps[] = [ }, ]; -// Particle effect for the logo -const ParticleEffect = () => { - return ( -
- {[...Array(5)].map((_, i) => ( - - ))} -
- ); -}; - export const Navbar = () => { const [isOpen, setIsOpen] = useState(false); const [scrolled, setScrolled] = useState(false); @@ -76,37 +44,38 @@ export const Navbar = () => { useEffect(() => { const handleScroll = () => { setScrolled(window.scrollY > 10); - - // Update active section based on scroll - const sections = routeList - .map((route) => { - const id = route.href.substring(1); - const element = document.getElementById(id); - if (element) { - const rect = element.getBoundingClientRect(); - const isInView = - rect.top <= window.innerHeight / 2 && - rect.bottom >= window.innerHeight / 2; - return { id, isInView }; - } - return null; - }) - .filter(Boolean); - - const currentSection = sections.find((section) => section?.isInView)?.id; - if (currentSection) { - setActiveSection(currentSection); - } }; - window.addEventListener("scroll", handleScroll); + window.addEventListener("scroll", handleScroll, { passive: true }); handleScroll(); return () => window.removeEventListener("scroll", handleScroll); }, []); + useEffect(() => { + // Section active = celle qui traverse la ligne médiane du viewport, + // sans getBoundingClientRect à chaque event scroll (AUDIT.md P5) + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setActiveSection(entry.target.id); + } + }); + }, + { rootMargin: "-50% 0px -50% 0px" } + ); + + routeList.forEach((route) => { + const element = document.getElementById(route.href.substring(1)); + if (element) observer.observe(element); + }); + + return () => observer.disconnect(); + }, []); + return ( - { - - - {/* Glow effect */} - - - + + {/* Mobile navigation */}
@@ -169,7 +137,7 @@ export const Navbar = () => { - { Menu Icon {/* Notification dot */} - - +
+ {
{routeList.map(({ href, label }: RouteProps, index) => ( - { {label} - + ))}
- { Github
- +
@@ -259,7 +217,7 @@ export const Navbar = () => {
{" "} {/* plain div, no motion */} - { {isActive && ( )} - +
); })}
- { Github
- + - - +
-
+ ); }; diff --git a/src/main.tsx b/src/main.tsx index 3952ee2..687a754 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,16 +1,29 @@ import React from "react"; import ReactDOM from "react-dom/client"; +import { LazyMotion, domAnimation, MotionConfig } from "framer-motion"; import App from "./App.tsx"; import { ThemeProvider } from "@/providers/theme-provider.tsx"; import { LanguageProvider } from "@/components/language.tsx"; -import "./i18n"; +import "./i18n"; +// Fonts auto-hébergées (AUDIT.md P6) — uniquement variantes et sous-ensembles latins utilisés, +// font-display: swap inclus (DotGothic16 complet = ~120 subsets japonais inutiles ici) +import "@fontsource/dotgothic16/latin-400.css"; +import "@fontsource/space-mono/latin-400.css"; +import "@fontsource/space-mono/latin-700.css"; +import "@fontsource/space-mono/latin-ext-400.css"; +import "@fontsource/space-mono/latin-ext-700.css"; import "./index.css"; ReactDOM.createRoot(document.getElementById("root")!).render( - + {/* strict: seuls les composants `m.` (chargés à la demande) sont autorisés — voir AUDIT.md P1 */} + + + + + diff --git a/vite.config.ts b/vite.config.ts index 87893b0..14fa60f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,6 +5,17 @@ import path from "path" // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], + build: { + rollupOptions: { + output: { + manualChunks: { + react: ["react", "react-dom"], + motion: ["framer-motion"], + i18n: ["i18next", "react-i18next", "i18next-browser-languagedetector"], + }, + }, + }, + }, resolve: { alias: { "@": path.resolve(__dirname, "./src"), From 2c3aedb5344c89e62cab8200dc1bd7d592418df3 Mon Sep 17 00:00:00 2001 From: EdGhi Date: Sat, 15 Aug 2026 00:37:58 +0100 Subject: [PATCH 11/17] Fix A1-A5: accessibilite (contenu hover, FAQ Radix, burger, titres, contrastes) - A1: Applications - description + tech stack visibles par defaut (plus d'overlay hover-only); Sponsors - "Visit" declenche aussi au focus clavier - A2: FAQ - accordion Radix (ui/accordion) a la place du bouton fait main, aria-expanded/aria-controls et clavier fournis, style conserve - A3: sr-only "Open menu" enfant direct du SheetTrigger, icone aria-hidden - A4: deja traite avec P2 (flou anime des titres supprime) - A5: un seul h1 (hero fusionne), Newsletter h3->h2, compteurs Statistics h2->div; --secondary clair 59%->32% et hero to-[#B8860B] en clair (AA texte large); --muted clair remis en gris neutre; PyConBanner 9px->11px Co-Authored-By: Claude Fable 5 --- AUDIT.md | 24 ++-- src/App.css | 6 +- src/components/PyConBanner.tsx | 2 +- src/components/Statistics.tsx | 5 +- src/containers/Applications.tsx | 86 ++++---------- src/containers/FAQ.tsx | 193 ++++++++++++-------------------- src/containers/Hero.tsx | 45 ++++---- src/containers/Newsletter.tsx | 4 +- src/containers/Sponsors.tsx | 3 + src/layouts/Navbar.tsx | 5 +- 10 files changed, 140 insertions(+), 233 deletions(-) diff --git a/AUDIT.md b/AUDIT.md index 1682e6b..cfb9a21 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -100,26 +100,26 @@ Dépendances : B1 avant toute retouche visuelle (le design actuel est partiellem ## A — Accessibilité -### A1 — Contenu essentiel uniquement au hover 🟡 +### A1 — Contenu essentiel uniquement au hover 🟡 — ✅ traité - **`src/containers/Applications.tsx`** : descriptions + tech stack visibles seulement au survol (« Hover over each section to learn more ») → invisible au clavier et sur tactile (majorité du trafic). **`src/containers/Sponsors.tsx`** : « Visit » uniquement au hover. -- **Correction :** afficher le contenu par défaut, ou le rendre toggleable au tap/focus (et déclencher aussi sur `onFocus`). +- **Correction appliquée :** Applications — description + tech stack complet affichés en permanence dans un `CardContent` ; overlay hover-only, badges hover sur l'image et mention « Hover over each section » supprimés. Sponsors — `onFocus`/`onBlur` sur le lien déclenchent les mêmes révélations que le hover (le « Visit » apparaît au focus clavier ; au tactile, le tap navigue directement). -### A2 — FAQ accordéon fait main sans ARIA alors que Radix est installé 🟡 +### A2 — FAQ accordéon fait main sans ARIA alors que Radix est installé 🟡 — ✅ traité - **Fichier :** `src/containers/FAQ.tsx:240` (bouton custom sans `aria-expanded`/`aria-controls`). -- **Correction :** utiliser `src/components/ui/accordion.tsx` (wrapper Radix déjà présent et inutilisé) — accessibilité gratuite, moins de code. Conserver le style visuel actuel. +- **Correction appliquée :** accordéon remplacé par `ui/accordion.tsx` (Radix, `type="multiple"`) — ARIA + navigation clavier fournis. Style visuel conservé (cartes, halo hover, icône HelpCircle) ; le chevron tourne via `data-state` Radix. -### A3 — Bouton burger sans nom accessible 🟡 +### A3 — Bouton burger sans nom accessible 🟡 — ✅ traité - **Fichier :** `src/layouts/Navbar.tsx:182–187` : le `` est enfant de l'icône SVG lucide au lieu du bouton. -- **Correction :** déplacer le sr-only comme enfant direct du `SheetTrigger` (ou `aria-label="Ouvrir le menu"` sur le trigger). +- **Correction appliquée :** `Open menu` déplacé en enfant direct du `SheetTrigger` ; icône `Menu` passée en `aria-hidden`. -### A4 — Flou animé permanent sur les titres 🟡 +### A4 — Flou animé permanent sur les titres 🟡 — ✅ traité (avec P2) - **Constat :** quasi tous les titres ont `animate={{ filter: ["blur(0px)", "blur(0.5px)", "blur(0px)"] }}` en boucle → texte légèrement flou en continu, fatigue visuelle. -- **Correction :** supprimer cet effet partout (Hero, Sponsors, HowItWorks, Services, Newsletter, Team, FAQ, Applications, Footer). Se combine avec P2. +- **Correction appliquée :** effet supprimé partout lors du traitement de P2. -### A5 — Hiérarchie de titres + contrastes + `--muted` jaune vif 🟡 -- **Titres :** hero = h1 « Python is » + h2 « Fun! » (à fusionner en un seul h1) ; Newsletter titre en h3 ; compteurs Statistics en h2 (→ `p` ou `div`). Une seule h1 par page, h2 pour les sections. -- **Contraste :** vérifier le jaune `--secondary` en texte/dégradé sur fond clair (WCAG AA 4.5:1) ; `text-[9px]` dans PyConBanner trop petit → 11–12 px min. -- **`src/App.css:83`** : `--muted: 50, 96%, 59%` = jaune vif pour une couleur « muted » → remettre un gris neutre (ex. `240 4.8% 95.9%`) et vérifier les usages existants de `bg-muted`. +### A5 — Hiérarchie de titres + contrastes + `--muted` jaune vif 🟡 — ✅ traité +- **Titres :** hero fusionné en un seul h1 (deux lignes en `span.block`) ; Newsletter h3 → h2 ; compteurs Statistics h2 → `div`. Une seule h1 par page, h2 pour les sections, vérifié par grep. +- **Contraste :** `--secondary` clair assombri `50 96% 59%` → `50 96% 32%` (~3.6:1 sur blanc, AA texte large ; l'ancien jaune tenait ~1.5:1 — le `.dark` garde le jaune vif). Hero : `to-[#FFE873]` limité au dark (`to-[#B8860B]` en clair). PyConBanner : `text-[9px]` → `text-[11px] sm:text-xs`. ⚠️ Passe visuelle light recommandée : le jaune de marque devient doré foncé en thème clair partout où il sert de texte/fond. +- **`--muted` clair :** `50 96% 59%` → gris neutre `240 4.8% 95.9%` (impacte input Newsletter `bg-muted/50` et hover cartes Statistics — désormais gris au lieu de jaune). --- diff --git a/src/App.css b/src/App.css index c2b5075..fcc849b 100644 --- a/src/App.css +++ b/src/App.css @@ -78,9 +78,11 @@ --popover-foreground: 240 10% 3.9%; --primary: 166 95% 29%; --primary-foreground: 355.7 100% 97.3%; - --secondary: 50 96% 59%; + /* Jaune assombri en thème clair : l'ancien 50 96% 59% ne tenait que + ~1.5:1 sur fond blanc en texte/dégradé de titres (AUDIT.md A5) */ + --secondary: 50 96% 32%; --secondary-foreground: 240 5.9% 10%; - --muted: 50 96% 59%; + --muted: 240 4.8% 95.9%; --muted-foreground: 240 3.8% 46.1%; --accent: 50 96% 59%; --accent-foreground: 240 5.9% 10%; diff --git a/src/components/PyConBanner.tsx b/src/components/PyConBanner.tsx index 922a55b..75f5acb 100644 --- a/src/components/PyConBanner.tsx +++ b/src/components/PyConBanner.tsx @@ -26,7 +26,7 @@ const TimeUnit = ({ value, label }: { value: number; label: string }) => ( {String(value).padStart(2, "0")} - + {label}
diff --git a/src/components/Statistics.tsx b/src/components/Statistics.tsx index 725055a..1f86778 100644 --- a/src/components/Statistics.tsx +++ b/src/components/Statistics.tsx @@ -84,7 +84,8 @@ export const Statistics = () => { }; return ( - { "radial-gradient(circle, rgba(var(--primary-rgb), 1) 0%, transparent 70%)" }} /> - + ); }; diff --git a/src/containers/Applications.tsx b/src/containers/Applications.tsx index c072d38..3823b96 100644 --- a/src/containers/Applications.tsx +++ b/src/containers/Applications.tsx @@ -1,7 +1,7 @@ import { useState, useRef } from "react"; import { m, useInView, AnimatePresence } from "framer-motion"; -import { Card, CardHeader, CardTitle } from "@/components/ui/card"; -import { Code2, Database, Brain, Zap, Shield, Gamepad2, Sparkles, ArrowRight, Info } from "lucide-react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Code2, Database, Brain, Zap, Shield, Gamepad2, Sparkles, ArrowRight } from "lucide-react"; // Images servies localement en WebP 800px au lieu d'Unsplash ~2000px (AUDIT.md P3) import webDevelopmentImg from "@/assets/applications/web-development.webp"; import dataScienceImg from "@/assets/applications/data-science.webp"; @@ -169,10 +169,6 @@ export const Applications = () => { transition={{ delay: 0.6, duration: 0.8 }} > Python is used in various fields, from web development to artificial intelligence. -
- - Hover over each section to learn more. - @@ -295,30 +291,6 @@ export const Applications = () => { }} /> - {/* Tech stack badges */} - - {hovered === index && ( - - {techStack.slice(0, 3).map((tech, techIndex) => ( - - {tech} - - ))} - - )} -