fix(a11y): améliorer l'accessibilité WAVE - corrections majeures

- Corriger les erreurs de contraste (text-primary-300 → text-primary-600)
- Corriger la hiérarchie des titres (h3 → h2 pour GamePeriod et GrandPrize)
- Supprimer les alt redondants sur les images décoratives (PrizeCard)
- Ajouter aria-hidden sur les images décoratives
- Ajouter aria-label descriptif sur le lien logo du Header
- Utiliser les balises <time> pour les dates sémantiques
- Convertir les textes importants en titres appropriés (h3, h4)
- Mettre à jour les tests PrizeCard

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
soufiane 2025-12-07 15:53:47 +01:00
parent 02ec8a6bc8
commit bb7148b26a
8 changed files with 53 additions and 52 deletions

View File

@ -19,7 +19,6 @@ jest.mock('next/image', () => ({
describe('PrizeCard', () => {
const defaultProps = {
imageSrc: '/images/lots/infuseur.png',
imageAlt: 'Infuseur à thé premium',
badge: '60%',
title: 'Infuseur à thé premium',
description: 'Un infuseur en acier inoxydable de haute qualité',
@ -31,14 +30,14 @@ describe('PrizeCard', () => {
expect(screen.getByText('Infuseur à thé premium')).toBeInTheDocument();
expect(screen.getByText('Un infuseur en acier inoxydable de haute qualité')).toBeInTheDocument();
expect(screen.getByText('60%')).toBeInTheDocument();
expect(screen.getByAltText('Infuseur à thé premium')).toBeInTheDocument();
});
it('should render image with correct src', () => {
render(<PrizeCard {...defaultProps} />);
it('should render image with correct src and empty alt for decorative image', () => {
const { container } = render(<PrizeCard {...defaultProps} />);
const image = screen.getByAltText('Infuseur à thé premium');
const image = container.querySelector('img');
expect(image).toHaveAttribute('src', '/images/lots/infuseur.png');
expect(image).toHaveAttribute('alt', '');
});
it('should render with default styling for non-grand prix', () => {
@ -72,11 +71,13 @@ describe('PrizeCard', () => {
it('should have correct structure with image container and content', () => {
const { container } = render(<PrizeCard {...defaultProps} />);
// Check image container exists
expect(container.querySelector('.aspect-square')).toBeInTheDocument();
// Check image container exists with aria-hidden for decorative image
const imageContainer = container.querySelector('.aspect-square');
expect(imageContainer).toBeInTheDocument();
expect(imageContainer).toHaveAttribute('aria-hidden', 'true');
// Check title is h3
const title = screen.getByText('Infuseur à thé premium');
expect(title.tagName).toBe('H3');
const title = screen.getByRole('heading', { level: 3 });
expect(title).toHaveTextContent('Infuseur à thé premium');
});
});

View File

@ -9,12 +9,12 @@ export const metadata: Metadata = {
};
const PRIZES = [
{ imageSrc: "/images/lots/infuseur.png", imageAlt: "Infuseur à thé premium", badge: "60% des lots", title: "Infuseur à thé premium", description: "Un infuseur en acier inoxydable de haute qualité pour ressortir les arômes de vos thés en vrac" },
{ imageSrc: "/images/lots/the-detox.png", imageAlt: "Boîte 100g thé détox", badge: "20% des lots", title: "Boîte 100g thé détox", description: "Mélange détox aux plantes bio : menthe, citronnelle, fenouil et gingembre" },
{ imageSrc: "/images/lots/the-signature.png", imageAlt: "Boîte 100g thé signature", badge: "10% des lots", title: "Boîte 100g thé signature", description: "Notre mélange signature exclusif : Earl Grey aux agrumes et pétales de fleurs" },
{ imageSrc: "/images/lots/coffret-39.png", imageAlt: "Coffret découverte 39€", badge: "6% des lots", title: "Coffret découverte 39€", description: "Sélection de nos 3 thés premium dans un élégant coffret cadeau" },
{ imageSrc: "/images/lots/coffret-69.jpg", imageAlt: "Coffret prestige 69€", badge: "4% des lots", title: "Coffret prestige 69€", description: "Collection premium : 5 thés d'exception avec accessoires dans un coffret luxe" },
{ imageSrc: "/images/lots/grand-prix.png", imageAlt: "Grand prix - 1 an de thé", badge: "1 an de THÉ", title: "Tirage Final", description: "Livraison mensuelle pendant 12 mois", isGrandPrix: true },
{ imageSrc: "/images/lots/infuseur.png", imageAlt: "", badge: "60% des lots", title: "Infuseur à thé premium", description: "Un infuseur en acier inoxydable de haute qualité pour ressortir les arômes de vos thés en vrac" },
{ imageSrc: "/images/lots/the-detox.png", imageAlt: "", badge: "20% des lots", title: "Boîte 100g thé détox", description: "Mélange détox aux plantes bio : menthe, citronnelle, fenouil et gingembre" },
{ imageSrc: "/images/lots/the-signature.png", imageAlt: "", badge: "10% des lots", title: "Boîte 100g thé signature", description: "Notre mélange signature exclusif : Earl Grey aux agrumes et pétales de fleurs" },
{ imageSrc: "/images/lots/coffret-39.png", imageAlt: "", badge: "6% des lots", title: "Coffret découverte 39€", description: "Sélection de nos 3 thés premium dans un élégant coffret cadeau" },
{ imageSrc: "/images/lots/coffret-69.jpg", imageAlt: "", badge: "4% des lots", title: "Coffret prestige 69€", description: "Collection premium : 5 thés d'exception avec accessoires dans un coffret luxe" },
{ imageSrc: "/images/lots/grand-prix.png", imageAlt: "", badge: "1 an de THÉ", title: "Tirage Final", description: "Livraison mensuelle pendant 12 mois", isGrandPrix: true },
];
export default function LotsPage() {
@ -24,7 +24,7 @@ export default function LotsPage() {
<section className="bg-gradient-to-r from-white to-beige-50 py-12 border-b-2 border-beige-300">
<div className="container mx-auto px-4">
<div className="max-w-4xl mx-auto text-center">
<h1 className="text-4xl md:text-5xl font-bold text-primary-300 mb-4">
<h1 className="text-4xl md:text-5xl font-bold text-primary-600 mb-4">
Lots à gagner
</h1>
<p className="text-lg text-gray-600">
@ -41,11 +41,11 @@ export default function LotsPage() {
<div className="max-w-5xl mx-auto">
<div className="bg-gradient-to-r from-primary-500 to-primary-600 rounded-2xl p-8 shadow-2xl border-2 border-primary-400">
<div className="flex flex-col md:flex-row items-center gap-6">
<div className="flex-shrink-0">
<div className="flex-shrink-0" aria-hidden="true">
<div className="w-32 h-32 bg-white rounded-lg flex items-center justify-center p-3 shadow-md">
<Image
src="/images/lots/grand-prix.png"
alt="Grand prix"
alt=""
width={128}
height={128}
className="w-full h-full object-contain object-center"

View File

@ -13,12 +13,12 @@ import { ROUTES } from "@/utils/constants";
import { useRouter } from "next/navigation";
const HOME_PRIZES = [
{ imageSrc: "/images/lots/infuseur.png", imageAlt: "Infuseur à thé premium", badge: "60%", title: "Infuseur à thé premium", description: "Un infuseur en acier inoxydable de haute qualité pour ressortir les arômes de vos thés en vrac" },
{ imageSrc: "/images/lots/the-detox.png", imageAlt: "Boîte 100g thé détox", badge: "20%", title: "Boîte 100g thé détox", description: "Mélange détox aux plantes bio" },
{ imageSrc: "/images/lots/the-signature.png", imageAlt: "Boîte 100g thé signature", badge: "10%", title: "Boîte 100g thé signature", description: "Notre mélange signature exclusif" },
{ imageSrc: "/images/lots/coffret-39.png", imageAlt: "Coffret découverte 39€", badge: "6%", title: "Coffret découverte 39€", description: "Sélection de nos meilleurs thés" },
{ imageSrc: "/images/lots/coffret-69.jpg", imageAlt: "Coffret prestige 69€", badge: "4%", title: "Coffret prestige 69€", description: "Une expérience complète" },
{ imageSrc: "/images/lots/grand-prix.png", imageAlt: "Grand prix - 1 an de thé", badge: "1 an de THÉ", title: "Tirage Final", description: "Livraison mensuelle pendant 12 mois", isGrandPrix: true },
{ imageSrc: "/images/lots/infuseur.png", imageAlt: "", badge: "60%", title: "Infuseur à thé premium", description: "Un infuseur en acier inoxydable de haute qualité pour ressortir les arômes de vos thés en vrac" },
{ imageSrc: "/images/lots/the-detox.png", imageAlt: "", badge: "20%", title: "Boîte 100g thé détox", description: "Mélange détox aux plantes bio" },
{ imageSrc: "/images/lots/the-signature.png", imageAlt: "", badge: "10%", title: "Boîte 100g thé signature", description: "Notre mélange signature exclusif" },
{ imageSrc: "/images/lots/coffret-39.png", imageAlt: "", badge: "6%", title: "Coffret découverte 39€", description: "Sélection de nos meilleurs thés" },
{ imageSrc: "/images/lots/coffret-69.jpg", imageAlt: "", badge: "4%", title: "Coffret prestige 69€", description: "Une expérience complète" },
{ imageSrc: "/images/lots/grand-prix.png", imageAlt: "", badge: "1 an de THÉ", title: "Tirage Final", description: "Livraison mensuelle pendant 12 mois", isGrandPrix: true },
];
export default function HomePage() {
@ -58,8 +58,8 @@ export default function HomePage() {
<h1 className="text-3xl sm:text-4xl md:text-6xl lg:text-7xl font-bold text-primary-500 mb-4 md:mb-6 leading-tight drop-shadow-sm tracking-wide">
Jeu Concours - Thé Tip Top
</h1>
<p className="text-xl sm:text-2xl md:text-5xl lg:text-6xl font-semibold text-primary-300 mb-4 md:mb-6 leading-snug md:leading-relaxed tracking-wide">
Célébrons l'ouverture de notre 10<sup>ème</sup> boutique à <span className="text-primary-500 font-bold">Nice</span>
<p className="text-xl sm:text-2xl md:text-5xl lg:text-6xl font-semibold text-primary-600 mb-4 md:mb-6 leading-snug md:leading-relaxed tracking-wide">
Célébrons l'ouverture de notre 10<sup>ème</sup> boutique à <span className="text-primary-700 font-bold">Nice</span>
</p>
<p className="text-base sm:text-lg md:text-2xl lg:text-3xl text-gray-500 mb-8 md:mb-10 leading-relaxed tracking-wide px-2">
Participez à notre concours - 100% des participants gagnent un lot !
@ -106,7 +106,7 @@ export default function HomePage() {
<section className="py-20 bg-white/60 backdrop-blur-sm">
<div className="container mx-auto px-4">
<div className="max-w-5xl mx-auto">
<h2 className="text-4xl md:text-5xl font-bold text-center text-primary-300 mb-4">
<h2 className="text-4xl md:text-5xl font-bold text-center text-primary-600 mb-4">
Comment participer?
</h2>
<p className="text-center text-gray-600 mb-16 text-lg max-w-2xl mx-auto">
@ -170,7 +170,7 @@ export default function HomePage() {
<section className="py-20 bg-transparent">
<div className="container mx-auto px-4">
<div className="max-w-6xl mx-auto">
<h2 className="text-4xl md:text-5xl font-bold text-center text-primary-300 mb-4">
<h2 className="text-4xl md:text-5xl font-bold text-center text-primary-600 mb-4">
Lots à gagner
</h2>
<p className="text-center text-gray-600 mb-16 text-lg max-w-3xl mx-auto">

View File

@ -46,7 +46,7 @@ export default function Footer() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{/* Company Info */}
<div>
<div className="mb-4">
<div className="mb-4" aria-hidden="true">
<Logo variant="default" size="md" showText={true} />
</div>
<p className="text-sm text-beige-700 mb-4">

View File

@ -11,15 +11,15 @@ export default function GamePeriod() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z" />
</svg>
</div>
<h3 className="text-2xl md:text-3xl font-bold text-gray-900 mb-4">Période de validation des tickets</h3>
<h2 className="text-2xl md:text-3xl font-bold text-gray-900 mb-4">Période de validation des tickets</h2>
<p className="text-lg text-gray-600 mb-6">
Achetez et validez votre code de participation
</p>
<div className="bg-secondary-100 rounded-2xl px-6 py-4 w-full">
<p className="text-lg font-bold text-secondary-700">
Du 1 décembre 2025 au 31 décembre 2025
</p>
<p className="text-secondary-500 font-semibold mt-1">30 jours</p>
<h3 className="text-lg font-bold text-secondary-700">
Du <time dateTime="2025-12-01">1 décembre 2025</time> au <time dateTime="2025-12-31">31 décembre 2025</time>
</h3>
<p className="text-secondary-700 font-semibold mt-1">30 jours</p>
</div>
</div>
</div>
@ -32,15 +32,15 @@ export default function GamePeriod() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v13m0-13V6a2 2 0 112 2h-2zm0 0V5.5A2.5 2.5 0 109.5 8H12zm-7 4h14M5 12a2 2 0 110-4h14a2 2 0 110 4M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7" />
</svg>
</div>
<h3 className="text-2xl md:text-3xl font-bold text-gray-900 mb-4">Période de récupération des lots</h3>
<h2 className="text-2xl md:text-3xl font-bold text-gray-900 mb-4">Période de récupération des lots</h2>
<p className="text-lg text-gray-600 mb-6">
Récupérez vos lots gagnés en boutique
</p>
<div className="bg-primary-50 rounded-2xl px-6 py-4 w-full">
<p className="text-lg font-bold text-primary-700">
Du 1 décembre 2025 au 31 janvier 2026
</p>
<p className="text-primary-500 font-semibold mt-1">60 jours</p>
<h3 className="text-lg font-bold text-primary-700">
Du <time dateTime="2025-12-01">1 décembre 2025</time> au <time dateTime="2026-01-31">31 janvier 2026</time>
</h3>
<p className="text-primary-600 font-semibold mt-1">60 jours</p>
</div>
</div>
</div>

View File

@ -28,15 +28,15 @@ export default function GrandPrize({
</div>
{/* Titre */}
<h3 className="text-3xl md:text-4xl font-bold text-gray-900 mb-6">
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-6">
Grand Prix à gagner !
</h3>
</h2>
{/* Prix principal */}
<div className="bg-white/80 backdrop-blur-sm rounded-2xl px-8 py-6 mb-6 shadow-lg">
<p className="text-2xl md:text-3xl font-bold text-primary-500 mb-2">
<h3 className="text-2xl md:text-3xl font-bold text-primary-500 mb-2">
1 an de thé offert
</p>
</h3>
<p className="text-xl text-secondary-600 font-semibold">
d'une valeur de {prizeAmount}
</p>
@ -52,25 +52,25 @@ export default function GrandPrize({
{drawDate && (
<div className="bg-white/70 backdrop-blur-sm rounded-2xl p-5 flex items-center gap-4 shadow-md">
<div className="bg-primary-100 rounded-full p-3">
<svg className="w-6 h-6 text-primary-500" fill="currentColor" viewBox="0 0 20 20">
<svg className="w-6 h-6 text-primary-500" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
<path fillRule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clipRule="evenodd" />
</svg>
</div>
<div className="text-left">
<p className="text-sm text-gray-500 font-medium">Tirage au sort</p>
<p className="text-lg font-bold text-gray-700">{formatDate(drawDate)}</p>
<h4 className="text-sm text-gray-500 font-medium">Tirage au sort</h4>
<p className="text-lg font-bold text-gray-700"><time dateTime={drawDate.toISOString().split('T')[0]}>{formatDate(drawDate)}</time></p>
</div>
</div>
)}
<div className="bg-white/70 backdrop-blur-sm rounded-2xl p-5 flex items-center gap-4 shadow-md">
<div className="bg-primary-100 rounded-full p-3">
<svg className="w-6 h-6 text-primary-500" fill="currentColor" viewBox="0 0 20 20">
<svg className="w-6 h-6 text-primary-500" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
<path fillRule="evenodd" d="M6.267 3.455a3.066 3.066 0 001.745-.723 3.066 3.066 0 013.976 0 3.066 3.066 0 001.745.723 3.066 3.066 0 012.812 2.812c.051.643.304 1.254.723 1.745a3.066 3.066 0 010 3.976 3.066 3.066 0 00-.723 1.745 3.066 3.066 0 01-2.812 2.812 3.066 3.066 0 00-1.745.723 3.066 3.066 0 01-3.976 0 3.066 3.066 0 00-1.745-.723 3.066 3.066 0 01-2.812-2.812 3.066 3.066 0 00-.723-1.745 3.066 3.066 0 010-3.976 3.066 3.066 0 00.723-1.745 3.066 3.066 0 012.812-2.812zm7.44 5.252a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
</div>
<div className="text-left">
<p className="text-sm text-gray-500 font-medium">Certification</p>
<h4 className="text-sm text-gray-500 font-medium">Certification</h4>
<p className="text-lg font-bold text-gray-700">Contrôle d'huissier</p>
</div>
</div>

View File

@ -49,7 +49,7 @@ export default function Header() {
<div className="container mx-auto px-4">
<div className="flex items-center justify-between h-18 gap-4">
{/* Logo */}
<Link href={ROUTES.HOME} className="group flex items-center gap-3 flex-shrink-0">
<Link href={ROUTES.HOME} className="group flex items-center gap-3 flex-shrink-0" aria-label="Thé Tip Top - Retour à l'accueil">
<Logo size="md" showText={false} className="group-hover:scale-105 transition-transform" />
</Link>

View File

@ -6,7 +6,7 @@ import { cn } from '@/utils/helpers';
interface PrizeCardProps {
imageSrc: string;
imageAlt: string;
imageAlt?: string;
badge: string;
title: string;
description: string;
@ -16,7 +16,6 @@ interface PrizeCardProps {
export const PrizeCard: React.FC<PrizeCardProps> = ({
imageSrc,
imageAlt,
badge,
title,
description,
@ -38,10 +37,11 @@ export const PrizeCard: React.FC<PrizeCardProps> = ({
? 'bg-gradient-to-br from-primary-50 to-primary-100'
: 'bg-gradient-to-br from-beige-50 to-beige-100'
)}
aria-hidden="true"
>
<Image
src={imageSrc}
alt={imageAlt}
alt=""
width={400}
height={400}
className="w-full h-full object-contain object-center"