feat: add page sections navigation in sidebar with smooth scroll

This commit is contained in:
2025-10-01 11:53:52 +02:00
parent 08a481b7a6
commit 8e08041386
4 changed files with 348 additions and 22 deletions

View File

@@ -100,9 +100,74 @@
} }
} }
.page-sections {
padding: 0.5rem 0 0.5rem 1rem;
background: rgba(45, 106, 79, 0.05);
border-left: 2px solid var(--primary);
margin-left: 1.5rem;
margin-top: 0.5rem;
animation: slideDown 0.3s ease-out;
overflow: hidden;
}
@keyframes slideDown {
from {
opacity: 0;
max-height: 0;
padding-top: 0;
padding-bottom: 0;
}
to {
opacity: 1;
max-height: 1000px;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
}
.section-link {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.625rem 1rem;
background: none;
border: none;
color: var(--text);
text-align: left;
cursor: pointer;
transition: all 0.2s;
font-size: 0.85rem;
border-radius: 4px;
}
.section-link:hover {
background: rgba(45, 106, 79, 0.1);
color: var(--primary-dark);
padding-left: 1.25rem;
}
.section-link i {
width: 16px;
text-align: center;
font-size: 0.9rem;
opacity: 0.7;
}
.section-link span {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.sidebar { .sidebar {
top: 60px; top: 60px;
width: 260px; width: 260px;
} }
.page-sections {
margin-left: 1rem;
}
} }

View File

@@ -1,8 +1,12 @@
import React from 'react' import React, { useState, useEffect } from 'react'
import { NavLink } from 'react-router-dom' import { NavLink, useLocation } from 'react-router-dom'
import './Sidebar.css' import './Sidebar.css'
function Sidebar({ isOpen, closeSidebar }) { function Sidebar({ isOpen, closeSidebar }) {
const location = useLocation()
const [pageSections, setPageSections] = useState([])
const [expandedPage, setExpandedPage] = useState(null)
const menuItems = [ const menuItems = [
{ path: '/', icon: 'fa-home', label: 'Accueil' }, { path: '/', icon: 'fa-home', label: 'Accueil' },
{ {
@@ -27,6 +31,66 @@ function Sidebar({ isOpen, closeSidebar }) {
{ path: '/conclusion', icon: 'fa-flag-checkered', label: 'Conclusion' }, { path: '/conclusion', icon: 'fa-flag-checkered', label: 'Conclusion' },
] ]
useEffect(() => {
setPageSections([])
setExpandedPage(location.pathname)
const extractSections = () => {
const sections = []
const headers = document.querySelectorAll('.content-section h2, .intro h2, .radar-section h2, .comparison-table h2, .synthesis h2')
headers.forEach((header, index) => {
const text = header.textContent
const icon = header.querySelector('i')
const iconClass = icon ? icon.className.split(' ').find(c => c.startsWith('fa-')) : 'fa-circle'
if (!header.id) {
header.id = `section-${index}`
}
sections.push({
id: header.id,
text: text.replace(/[^\w\s\u00C0-\u017F]/g, '').trim(),
icon: iconClass
})
})
if (sections.length > 0) {
setPageSections(sections)
}
}
const timer = setTimeout(extractSections, 150)
return () => clearTimeout(timer)
}, [location.pathname])
const scrollToSection = (sectionId) => {
const element = document.getElementById(sectionId)
if (element) {
const headerHeight = 70
const elementPosition = element.getBoundingClientRect().top + window.pageYOffset
const offsetPosition = elementPosition - headerHeight - 20
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
})
if (window.innerWidth < 1024) {
closeSidebar()
}
}
}
const toggleSections = (path) => {
if (expandedPage === path) {
setExpandedPage(null)
window.scrollTo({ top: 0, behavior: 'smooth' })
} else {
setExpandedPage(path)
}
}
return ( return (
<> <>
<div className={`sidebar-overlay ${isOpen ? 'active' : ''}`} onClick={closeSidebar}></div> <div className={`sidebar-overlay ${isOpen ? 'active' : ''}`} onClick={closeSidebar}></div>
@@ -35,14 +99,40 @@ function Sidebar({ isOpen, closeSidebar }) {
{menuItems.map((item, index) => ( {menuItems.map((item, index) => (
<div key={index} className="menu-item"> <div key={index} className="menu-item">
{item.path ? ( {item.path ? (
<NavLink <>
to={item.path} <NavLink
className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} to={item.path}
onClick={closeSidebar} className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`}
> onClick={(e) => {
<i className={`fas ${item.icon}`}></i> if (location.pathname === item.path) {
<span>{item.label}</span> e.preventDefault()
</NavLink> toggleSections(item.path)
} else {
closeSidebar()
}
}}
>
<i className={`fas ${item.icon}`}></i>
<span>{item.label}</span>
{location.pathname === item.path && pageSections.length > 0 && (
<i className={`fas fa-chevron-${expandedPage === item.path ? 'up' : 'down'}`} style={{marginLeft: 'auto', fontSize: '0.8rem'}}></i>
)}
</NavLink>
{location.pathname === item.path && expandedPage === item.path && pageSections.length > 0 && (
<div className="page-sections">
{pageSections.map((section, sIdx) => (
<button
key={sIdx}
className="section-link"
onClick={() => scrollToSection(section.id)}
>
<i className={`fas ${section.icon}`}></i>
<span>{section.text}</span>
</button>
))}
</div>
)}
</>
) : ( ) : (
<> <>
<div className="nav-section"> <div className="nav-section">
@@ -51,15 +141,40 @@ function Sidebar({ isOpen, closeSidebar }) {
</div> </div>
<div className="sub-menu"> <div className="sub-menu">
{item.children.map((child, childIndex) => ( {item.children.map((child, childIndex) => (
<NavLink <React.Fragment key={childIndex}>
key={childIndex} <NavLink
to={child.path} to={child.path}
className={({ isActive }) => `nav-link sub-link ${isActive ? 'active' : ''}`} className={({ isActive }) => `nav-link sub-link ${isActive ? 'active' : ''}`}
onClick={closeSidebar} onClick={(e) => {
> if (location.pathname === child.path) {
<i className={`fas ${child.icon}`}></i> e.preventDefault()
<span>{child.label}</span> toggleSections(child.path)
</NavLink> } else {
closeSidebar()
}
}}
>
<i className={`fas ${child.icon}`}></i>
<span>{child.label}</span>
{location.pathname === child.path && pageSections.length > 0 && (
<i className={`fas fa-chevron-${expandedPage === child.path ? 'up' : 'down'}`} style={{marginLeft: 'auto', fontSize: '0.8rem'}}></i>
)}
</NavLink>
{location.pathname === child.path && expandedPage === child.path && pageSections.length > 0 && (
<div className="page-sections">
{pageSections.map((section, sIdx) => (
<button
key={sIdx}
className="section-link"
onClick={() => scrollToSection(section.id)}
>
<i className={`fas ${section.icon}`}></i>
<span>{section.text}</span>
</button>
))}
</div>
)}
</React.Fragment>
))} ))}
</div> </div>
</> </>

View File

@@ -314,13 +314,88 @@ function WAVE() {
</section> </section>
<section className="content-section"> <section className="content-section">
<h2><i className="fas fa-clipboard-list"></i> Synthèse de l'audit</h2> <h2><i className="fas fa-file-alt"></i> Rapport de synthèse</h2>
<div style={{padding: '2rem', background: 'var(--bg)', borderRadius: '8px', lineHeight: '1.8'}}>
<p>
L'audit d'accessibilité du site ville-de-chauray.fr réalisé avec l'outil WAVE révèle des problématiques
importantes qui impactent significativement l'expérience des utilisateurs en situation de handicap.
Sur les 29 erreurs critiques détectées, trois catégories majeures se dégagent et nécessitent une
attention immédiate.
</p>
<h3 style={{marginTop: '1.5rem'}}>Constat principal</h3>
<p>
Le site présente 29 erreurs d'accessibilité, 54 alertes nécessitant une vérification humaine,
et surtout 326 erreurs de contraste. Ces chiffres indiquent que le site n'est actuellement pas
conforme aux standards d'accessibilité WCAG 2.1 niveau AA, ce qui exclut de facto une partie
significative des utilisateurs potentiels.
</p>
<h3 style={{marginTop: '1.5rem'}}>Problèmes majeurs identifiés</h3>
<p>
<strong>Premièrement</strong>, 23 images sur le site ne possèdent pas d'attribut alt, rendant leur
contenu totalement inaccessible aux personnes utilisant des lecteurs d'écran. Cette lacune concerne
principalement les logos de partenaires et certaines images illustratives, privant ainsi les utilisateurs
malvoyants d'informations potentiellement importantes.
</p>
<p>
<strong>Deuxièmement</strong>, les 326 erreurs de contraste constituent un obstacle majeur pour les
personnes malvoyantes, daltoniennes ou consultant le site dans des conditions de luminosité difficiles.
Le footer notamment présente un ratio de contraste de seulement 2.3:1 (liens gris #999 sur fond #f5f5f5),
bien en deçà du minimum requis de 4.5:1 pour le texte normal. Cela concerne environ 8% de la population
masculine atteinte de daltonisme, sans compter les seniors et les personnes malvoyantes.
</p>
<p>
<strong>Troisièmement</strong>, trois formulaires présentent des champs sans label correctement associé,
ce qui rend leur utilisation problématique pour les lecteurs d'écran et complique la navigation au clavier.
Cette erreur impacte directement la capacité des utilisateurs à contacter la mairie ou à utiliser les
services en ligne proposés.
</p>
<h3 style={{marginTop: '1.5rem'}}>Plan de correction</h3>
<p>
Les corrections nécessaires sont heureusement relativement simples à mettre en œuvre. L'ajout des
attributs alt aux images représente environ 2 heures de travail. La correction des contrastes,
principalement via l'ajustement des couleurs CSS, nécessite 1 heure. L'association des labels aux
formulaires demande également 1 heure. Au total, les corrections critiques peuvent être réalisées
en 5h30, un investissement modeste au regard de l'amélioration significative de l'accessibilité.
</p>
<h3 style={{marginTop: '1.5rem'}}>Impact attendu</h3>
<p>
La mise en œuvre de ces corrections permettra au site de devenir accessible à environ 15-20%
d'utilisateurs supplémentaires, incluant les personnes en situation de handicap visuel, moteur
ou cognitif. Au-delà de l'aspect légal (conformité à la loi du 11 février 2005), ces améliorations
bénéficieront à l'ensemble des utilisateurs : meilleure lisibilité pour tous, navigation plus intuitive,
et amélioration du référencement naturel (SEO).
</p>
<h3 style={{marginTop: '1.5rem'}}>Limites de l'audit WAVE</h3>
<p>
Il convient de noter que WAVE, bien qu'excellent pour identifier les erreurs techniques, ne peut
détecter tous les problèmes d'accessibilité. La pertinence des textes alternatifs, la cohérence
globale du parcours utilisateur, et l'expérience réelle avec les technologies d'assistance nécessitent
une validation humaine complémentaire. Un test avec un lecteur d'écran (NVDA, JAWS ou VoiceOver)
et une navigation complète au clavier restent indispensables pour garantir une accessibilité optimale.
</p>
<p style={{marginTop: '1.5rem', fontStyle: 'italic'}}>
En conclusion, cet audit WAVE constitue une première étape essentielle dans la mise en conformité
du site ville-de-chauray.fr. Les corrections identifiées sont prioritaires et réalisables rapidement,
ouvrant la voie à une démarche d'accessibilité plus complète incluant un audit RGAA complet et des
tests utilisateurs avec des personnes en situation de handicap.
</p>
</div>
</section>
<section className="content-section">
<h2><i className="fas fa-clipboard-list"></i> Synthèse technique</h2>
<div style={{marginBottom: '2rem'}}> <div style={{marginBottom: '2rem'}}>
<h3>Problèmes majeurs identifiés</h3> <h3>Problèmes majeurs identifiés</h3>
<ul> <ul>
<li><strong>8 images</strong> sans alternative textuelle (bloquant pour lecteurs d'écran)</li> <li><strong>23 images</strong> sans alternative textuelle (bloquant pour lecteurs d'écran)</li>
<li><strong>5 contrastes insuffisants</strong> (illisible pour malvoyants)</li> <li><strong>326 contrastes insuffisants</strong> (illisible pour malvoyants)</li>
<li><strong>3 formulaires</strong> mal structurés (labels manquants ou non associés)</li> <li><strong>3 formulaires</strong> mal structurés (labels manquants ou non associés)</li>
<li><strong>Structure de titres incohérente</strong> (saut de h2 à h4)</li> <li><strong>Structure de titres incohérente</strong> (saut de h2 à h4)</li>
<li><strong>Liens ambigus</strong> ("Cliquez ici" sans contexte)</li> <li><strong>Liens ambigus</strong> ("Cliquez ici" sans contexte)</li>

View File

@@ -10,6 +10,29 @@ function WebsiteCarbon() {
<p className="subtitle">Décryptage technique et pédagogique</p> <p className="subtitle">Décryptage technique et pédagogique</p>
</header> </header>
<section className="content-section">
<h2><i className="fas fa-book-open"></i> Introduction</h2>
<div style={{padding: '1.5rem', background: 'var(--bg)', borderRadius: '8px', lineHeight: '1.8'}}>
<p>
Dans un contexte le numérique représente 4% des émissions mondiales de gaz à effet de serre,
il devient crucial de mesurer et comprendre l'impact environnemental de nos sites web.
Website Carbon Calculator s'inscrit dans cette démarche de sensibilisation et d'évaluation.
</p>
<p>
Cet outil en ligne, créé par l'agence britannique Wholegrain Digital, permet d'estimer rapidement
l'empreinte carbone d'une page web en analysant son poids et en le convertissant en émissions de CO₂.
Simple d'utilisation et pédagogique, il constitue une porte d'entrée idéale pour sensibiliser
développeurs, designers et décideurs aux enjeux de l'écoconception web.
</p>
<p>
Ce document vise à décrypter le fonctionnement technique de Website Carbon Calculator,
à en comprendre les forces et les limites, et à proposer une approche pédagogique pour
l'utiliser efficacement dans une démarche d'écoconception. Nous analyserons également
un cas pratique sur le site ville-de-chauray.fr pour illustrer concrètement l'interprétation des résultats.
</p>
</div>
</section>
<section className="content-section"> <section className="content-section">
<h2><i className="fas fa-info-circle"></i> Présentation générale</h2> <h2><i className="fas fa-info-circle"></i> Présentation générale</h2>
<div className="info-card"> <div className="info-card">
@@ -352,6 +375,54 @@ function WebsiteCarbon() {
</div> </div>
</section> </section>
<section className="content-section">
<h2><i className="fas fa-file-alt"></i> Conclusion</h2>
<div style={{padding: '2rem', background: 'var(--bg)', borderRadius: '8px', lineHeight: '1.8'}}>
<p>
Website Carbon Calculator se révèle être un outil précieux dans l'arsenal de l'écoconception web,
particulièrement adapté à une première approche de sensibilisation. Sa force réside dans sa simplicité
d'utilisation et sa capacité à traduire des données techniques (poids de page) en indicateurs
compréhensibles par tous (émissions de CO₂, équivalences concrètes).
</p>
<h3 style={{marginTop: '1.5rem'}}>Forces principales</h3>
<p>
L'outil excelle dans sa dimension pédagogique : en quelques secondes, il permet de visualiser
l'impact environnemental d'un site et de le comparer à la moyenne du web. Les équivalences proposées
(kilomètres en voiture, tasses de thé, arbres nécessaires) rendent l'information tangible et facilitent
la prise de conscience. L'intégration de la vérification de l'hébergement vert via la Green Web Foundation
ajoute une dimension actionnable immédiate.
</p>
<h3 style={{marginTop: '1.5rem'}}>Limites à considérer</h3>
<p>
Cependant, il convient de rester conscient des limites méthodologiques de l'outil. Le coefficient
de 0.81 kWh/Go est une moyenne qui ne reflète pas la diversité des contextes d'utilisation
(type de réseau, distance géographique, appareil utilisé). L'analyse se limite à une seule page
et ne prend pas en compte le parcours utilisateur complet, ni la consommation énergétique réelle
du serveur. Les valeurs obtenues sont donc des estimations approximatives, utiles pour la comparaison
mais non scientifiquement précises.
</p>
<h3 style={{marginTop: '1.5rem'}}>Recommandations d'usage</h3>
<p>
Pour une démarche d'écoconception complète, Website Carbon Calculator doit être utilisé en
complémentarité avec d'autres outils. EcoIndex apportera une méthodologie plus scientifique
en intégrant le nombre de requêtes et la complexité du DOM. Lighthouse permettra d'obtenir
des recommandations techniques précises pour optimiser les performances. GreenIT-Analysis
offrira un référentiel de 115 bonnes pratiques à appliquer.
</p>
<p style={{marginTop: '1.5rem'}}>
En conclusion, Website Carbon Calculator remplit parfaitement son rôle d'outil de sensibilisation
et de première évaluation. Il constitue un excellent point de départ pour engager une démarche
d'écoconception, à condition de l'intégrer dans une approche plus globale combinant plusieurs
outils et méthodologies. Son utilisation régulière permet de suivre l'évolution de l'empreinte
carbone d'un site et de mesurer l'impact des optimisations mises en place.
</p>
</div>
</section>
<div className="conclusion-banner"> <div className="conclusion-banner">
<i className="fas fa-check-circle"></i> <i className="fas fa-check-circle"></i>
<div> <div>