feat: add SonarQube integration, cookie consent, and authentication improvements

- Add SonarQube configuration for code quality analysis
  - sonar-project.properties with TypeScript/Next.js settings
  - .sonarignore to exclude build artifacts and dependencies
  - npm run sonar script
  - Jenkins pipeline stages for SonarQube analysis and quality gate

- Implement cookie consent banner
  - New CookieConsent component with matching site colors
  - localStorage persistence for user choice
  - Accept/Reject buttons with proper styling
  - Link to cookies policy page

- Add strict authentication protection for game page
  - Redirect unauthenticated users to login from /jeux
  - Clean up redundant auth checks and UI elements
  - Preserve redirect parameter for post-login navigation

- Implement smart navigation with auth-aware redirects
  - "Jouer maintenant" button redirects based on auth status
  - "Participer au jeu" footer link with conditional routing
  - Authenticated users go to /jeux, others to /register

- UI improvements and cleanup
  - Remove "Voir les lots" button from homepage
  - Remove "Gestion des cookies" from footer
  - Remove "À propos" from footer navigation
  - Consistent design across components

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
soufiane 2025-11-21 01:23:50 +01:00
parent c8fdd59553
commit 7febb137e9
13 changed files with 255 additions and 131 deletions

50
.sonarignore Normal file
View File

@ -0,0 +1,50 @@
# Dependencies
node_modules/
**/node_modules/**
# Build outputs
.next/
out/
dist/
build/
# Testing
coverage/
.nyc_output/
# Static files
public/
# Configuration files
*.config.js
*.config.ts
next.config.js
tailwind.config.js
postcss.config.js
jest.config.js
jest.setup.js
# Environment files
.env*
!.env.example
# Logs
logs/
*.log
npm-debug.log*
# IDE
.vscode/
.idea/
# Docker
Dockerfile
docker-compose*.yml
# CI/CD
Jenkinsfile
.github/
# Backups
*.backup
*.bak

26
Jenkinsfile vendored
View File

@ -79,6 +79,32 @@ pipeline {
}
}
stage('SonarQube Analysis') {
agent {
docker {
image 'sonarsource/sonar-scanner-cli:latest'
args '-u root'
}
}
steps {
echo "🔍 Analyse de la qualité du code avec SonarQube..."
script {
withSonarQubeEnv('SonarQube') {
sh 'sonar-scanner'
}
}
}
}
stage('Quality Gate') {
steps {
echo "🚦 Vérification du Quality Gate SonarQube..."
timeout(time: 5, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
stage('Build Frontend') {
agent {
docker {

View File

@ -34,6 +34,13 @@ export default function JeuxPage() {
const [currentPrizeIndex, setCurrentPrizeIndex] = useState(0);
const [isSpinning, setIsSpinning] = useState(false);
// Protection stricte: redirection si non connecté
useEffect(() => {
if (!isAuthenticated) {
router.push(`${ROUTES.LOGIN}?redirect=/jeux`);
}
}, [isAuthenticated, router]);
const {
register,
handleSubmit,
@ -58,12 +65,6 @@ export default function JeuxPage() {
// Réinitialiser le message d'erreur
setErrorMessage("");
// Si non connecté, rediriger vers login
if (!isAuthenticated) {
router.push(`${ROUTES.LOGIN}?redirect=/jeux`);
return;
}
// Afficher la modal de roulette et démarrer l'animation
setShowRouletteModal(true);
setIsSpinning(true);
@ -122,21 +123,10 @@ export default function JeuxPage() {
</div>
<div className="p-8">
<div className="mb-6 text-center">
{isAuthenticated ? (
<p className="text-gray-700 text-lg">
Bonjour <span className="font-bold text-[#1a4d2e]">{user?.firstName}</span>,
entrez le code de 10 caractères présent sur votre ticket de caisse
</p>
) : (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-4">
<p className="text-yellow-800 text-sm">
💡 Vous devez être connecté pour valider votre code.
<Link href={ROUTES.LOGIN} className="font-semibold underline ml-1 hover:text-yellow-900">
Connectez-vous
</Link>
</p>
</div>
)}
<p className="text-gray-700 text-lg">
Bonjour <span className="font-bold text-[#1a4d2e]">{user?.firstName}</span>,
entrez le code de 10 caractères présent sur votre ticket de caisse
</p>
</div>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
@ -184,30 +174,15 @@ export default function JeuxPage() {
</div>
</form>
{!isAuthenticated && (
<div className="mt-6 text-center">
<p className="text-sm text-gray-600 mb-3">
Pas encore de compte ?
</p>
<Link href={ROUTES.REGISTER}>
<button className="border-2 border-[#1a4d2e] text-[#1a4d2e] hover:bg-[#1a4d2e] hover:text-white font-bold px-6 py-2 rounded-lg transition-all">
Créer un compte gratuitement
</button>
</Link>
</div>
)}
{isAuthenticated && (
<div className="mt-6 p-4 bg-blue-50 border-l-4 border-blue-500 rounded-lg">
<p className="text-sm text-blue-800 font-semibold mb-2">
💡 Bon à savoir :
</p>
<ul className="text-sm text-blue-700 space-y-1 list-disc list-inside">
<li>Chaque code ne peut être utilisé qu'une seule fois</li>
<li>Consultez vos tickets sur la page <Link href={ROUTES.HISTORY} className="underline font-medium hover:text-blue-900">Mes gains</Link></li>
</ul>
</div>
)}
<div className="mt-6 p-4 bg-blue-50 border-l-4 border-blue-500 rounded-lg">
<p className="text-sm text-blue-800 font-semibold mb-2">
💡 Bon à savoir :
</p>
<ul className="text-sm text-blue-700 space-y-1 list-disc list-inside">
<li>Chaque code ne peut être utilisé qu'une seule fois</li>
<li>Consultez vos tickets sur la page <Link href={ROUTES.HISTORY} className="underline font-medium hover:text-blue-900">Mes gains</Link></li>
</ul>
</div>
</div>
</div>
</div>

View File

@ -3,6 +3,7 @@
import { usePathname } from "next/navigation";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import CookieConsent from "@/components/CookieConsent";
export default function LayoutClient({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
@ -23,6 +24,7 @@ export default function LayoutClient({ children }: { children: React.ReactNode }
{children}
</main>
<Footer />
<CookieConsent />
</>
);
}

View File

@ -155,12 +155,13 @@ export default function LoginPage() {
{/* Tabs */}
<div className="flex border-b border-gray-200">
<button className="flex-1 py-4 px-6 text-center font-semibold text-gray-900 bg-white border-b-2 border-[#d4a574]">
Connexion
<button className="flex-1 py-4 px-6 text-center font-bold text-gray-900 bg-white border-b-3 border-[#d4a574] shadow-[0_3px_8px_rgba(212,165,116,0.3)] relative">
<span className="relative z-10">Connexion</span>
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-gradient-to-r from-transparent via-[#d4a574] to-transparent"></div>
</button>
<Link
href={ROUTES.REGISTER}
className="flex-1 py-4 px-6 text-center font-semibold text-gray-500 bg-gray-50 hover:bg-gray-100 transition-colors"
className="flex-1 py-4 px-6 text-center font-semibold text-gray-500 bg-gray-50 hover:bg-gradient-to-r hover:from-[#d4a574]/5 hover:to-[#c4956a]/5 hover:text-[#c4956a] transition-all duration-300 hover:shadow-inner"
>
Inscription
</Link>
@ -267,7 +268,7 @@ export default function LoginPage() {
<button
type="submit"
disabled={isSubmitting}
className="w-full bg-[#d4a574] hover:bg-[#c4956a] disabled:bg-gray-400 text-white font-bold px-8 py-4 rounded-lg transition-all"
className="w-full bg-gradient-to-r from-[#d4a574] to-[#c4956a] hover:from-[#e5b685] hover:to-[#d4a574] disabled:bg-gray-400 disabled:from-gray-400 disabled:to-gray-400 text-white font-bold px-8 py-4 rounded-lg transition-all duration-300 hover:shadow-[0_0_25px_rgba(212,165,116,0.7)] hover:scale-[1.02] shadow-lg transform"
>
{isSubmitting ? "Connexion..." : "Se connecter"}
</button>

View File

@ -8,9 +8,12 @@ import GamePeriod from "@/components/GamePeriod";
import GrandPrize from "@/components/GrandPrize";
import AboutContest from "@/components/AboutContest";
import { useState } from "react";
import { useAuth } from "@/contexts/AuthContext";
import { ROUTES } from "@/utils/constants";
export default function HomePage() {
const [animationKey] = useState(Date.now());
const { isAuthenticated } = useAuth();
return (
<div className="min-h-screen -mt-[4.5rem] relative">
@ -90,7 +93,7 @@ export default function HomePage() {
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center px-4">
<Link href="/register">
<Link href={isAuthenticated ? ROUTES.GAME : "/register"}>
<button className="bg-[#d4a574] hover:bg-[#c4956a] text-white font-bold text-lg px-12 py-5 rounded-full shadow-2xl transition-all w-full sm:w-auto hover:scale-105">
Jouer maintenant
</button>

View File

@ -114,12 +114,13 @@ export default function RegisterPage() {
<div className="flex border-b border-gray-200">
<Link
href={ROUTES.LOGIN}
className="flex-1 py-4 px-6 text-center font-semibold text-gray-500 bg-gray-50 hover:bg-gray-100 transition-colors"
className="flex-1 py-4 px-6 text-center font-semibold text-gray-500 bg-gray-50 hover:bg-gradient-to-r hover:from-[#d4a574]/5 hover:to-[#c4956a]/5 hover:text-[#c4956a] transition-all duration-300 hover:shadow-inner"
>
Connexion
</Link>
<button className="flex-1 py-4 px-6 text-center font-semibold text-gray-900 bg-white border-b-2 border-[#d4a574]">
Inscription
<button className="flex-1 py-4 px-6 text-center font-bold text-gray-900 bg-white border-b-3 border-[#d4a574] shadow-[0_3px_8px_rgba(212,165,116,0.3)] relative">
<span className="relative z-10">Inscription</span>
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-gradient-to-r from-transparent via-[#d4a574] to-transparent"></div>
</button>
</div>
@ -295,7 +296,7 @@ export default function RegisterPage() {
<button
type="submit"
disabled={isSubmitting}
className="w-full bg-[#d4a574] hover:bg-[#c4956a] disabled:bg-gray-400 text-white font-bold px-8 py-4 rounded-lg transition-all"
className="w-full bg-gradient-to-r from-[#d4a574] to-[#c4956a] hover:from-[#e5b685] hover:to-[#d4a574] disabled:bg-gray-400 disabled:from-gray-400 disabled:to-gray-400 text-white font-bold px-8 py-4 rounded-lg transition-all duration-300 hover:shadow-[0_0_25px_rgba(212,165,116,0.7)] hover:scale-[1.02] shadow-lg transform"
>
{isSubmitting ? "Inscription..." : "S'inscrire"}
</button>

View File

@ -24,65 +24,46 @@ export default function AboutContest({
`;
return (
<section className="py-20 bg-transparent">
<section className="py-20 bg-white/50 backdrop-blur-sm">
<div className="container mx-auto px-4">
<div className="max-w-7xl mx-auto">
<div className="grid md:grid-cols-2 gap-12 items-center">
<div className="grid md:grid-cols-2 gap-16 items-center">
{/* Texte */}
<div className="order-2 md:order-1">
<h2 className="text-4xl md:text-5xl font-bold text-gray-900 mb-6 leading-tight">
{title}
<div className="order-2 md:order-1 space-y-6">
<h2 className="text-4xl md:text-5xl font-bold text-[#5a5a4e] leading-tight">
Jeu Concours Thé Tip Top
<span className="block text-3xl md:text-4xl text-[#d4a574] mt-2">
Boutique Premium à Nice
</span>
</h2>
<div className="prose prose-lg text-gray-700 mb-8 space-y-4">
{(description || defaultDescription).split('\n').filter(p => p.trim()).map((paragraph, index) => (
<p key={index} className="leading-relaxed">
{paragraph.trim()}
<div className="space-y-4 text-lg text-gray-700">
<p className="leading-relaxed">
Bienvenue dans notre grand jeu-concours <span className="font-semibold text-[#5a5a4e]">Thé Tip Top</span> organisé à l'occasion de l'ouverture de notre <span className="font-semibold text-[#d4a574]">10ème boutique</span> de thé premium à Nice.
</p>
<p className="leading-relaxed">
Nous vous offrons la chance de gagner des produits exceptionnels : thés bio, accessoires exclusifs et bien plus encore.
</p>
<p className="leading-relaxed">
Participez facilement en quelques clics et tentez votre chance parmi nos lots prestigieux.
</p>
<div className="bg-gradient-to-r from-[#d4a574]/10 to-[#c4956a]/10 border-l-4 border-[#d4a574] p-4 rounded-r-lg">
<p className="font-semibold text-[#5a5a4e] text-xl">
100% de tickets gagnants ! Chaque participation vous offre la garantie de repartir avec un cadeau.
</p>
))}
</div>
{/* Points clés */}
<div className="space-y-3 mb-8">
<div className="flex items-start gap-3">
<svg className="w-6 h-6 text-[#1a4d2e] flex-shrink-0 mt-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 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>
<span className="text-gray-800">
<span className="font-semibold">100% de gagnants :</span> Chaque ticket acheté à 49 vous fait gagner un lot
</span>
</div>
<div className="flex items-start gap-3">
<svg className="w-6 h-6 text-[#1a4d2e] flex-shrink-0 mt-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 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>
<span className="text-gray-800">
<span className="font-semibold">Résultat immédiat :</span> Découvrez instantanément votre lot en ligne
</span>
</div>
<div className="flex items-start gap-3">
<svg className="w-6 h-6 text-[#1a4d2e] flex-shrink-0 mt-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 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>
<span className="text-gray-800">
<span className="font-semibold">Thés bio et artisanaux :</span> Tous nos produits sont de qualité premium
</span>
</div>
</div>
<Link href="/register">
<Button
size="lg"
className="bg-[#1a4d2e] hover:bg-[#2d5a3d] text-white font-bold shadow-xl"
>
En savoir plus
<svg className="w-5 h-5 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
</Button>
</Link>
<div className="flex flex-col sm:flex-row gap-4 pt-4">
<Link href="/register">
<button className="bg-gradient-to-r from-[#d4a574] to-[#c4956a] hover:from-[#e5b685] hover:to-[#d4a574] text-white font-bold text-lg px-8 py-4 rounded-lg transition-all duration-300 hover:shadow-[0_0_25px_rgba(212,165,116,0.7)] hover:scale-105 shadow-lg flex items-center justify-center gap-2">
En savoir plus
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
</button>
</Link>
</div>
</div>
{/* Image */}

View File

@ -0,0 +1,65 @@
'use client';
import { useState, useEffect } from 'react';
import Link from 'next/link';
export default function CookieConsent() {
const [showBanner, setShowBanner] = useState(false);
useEffect(() => {
// Vérifier si l'utilisateur a déjà donné son consentement
const consent = localStorage.getItem('cookieConsent');
if (!consent) {
setShowBanner(true);
}
}, []);
const handleAccept = () => {
localStorage.setItem('cookieConsent', 'accepted');
setShowBanner(false);
};
const handleReject = () => {
localStorage.setItem('cookieConsent', 'rejected');
setShowBanner(false);
};
if (!showBanner) return null;
return (
<div className="fixed bottom-0 left-0 right-0 z-50 bg-gradient-to-r from-[#f5f5f0] to-[#faf9f5] shadow-2xl border-t-4 border-[#d4a574]">
<div className="container mx-auto px-4 py-6">
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
{/* Message */}
<div className="flex-1 text-center md:text-left">
<p className="text-sm md:text-base text-[#5a5a4e]">
🍪 Ce site utilise des cookies pour améliorer votre expérience de navigation et analyser notre trafic.{' '}
<Link
href="/cookies"
className="text-[#d4a574] hover:text-[#c4956a] underline transition-colors font-semibold"
>
En savoir plus
</Link>
</p>
</div>
{/* Buttons */}
<div className="flex gap-3 flex-shrink-0">
<button
onClick={handleReject}
className="px-6 py-2.5 bg-white border-2 border-[#e5e4dc] hover:border-[#d4a574] text-[#5a5a4e] hover:text-[#d4a574] rounded-lg transition-all duration-300 text-sm font-semibold"
>
Refuser
</button>
<button
onClick={handleAccept}
className="px-6 py-2.5 bg-gradient-to-r from-[#d4a574] to-[#c4956a] hover:from-[#e5b685] hover:to-[#d4a574] text-white rounded-lg transition-all duration-300 shadow-lg hover:shadow-xl text-sm font-semibold"
>
Accepter tous les cookies
</button>
</div>
</div>
</div>
</div>
);
}

View File

@ -3,9 +3,11 @@
import Link from 'next/link';
import Logo from './Logo';
import { ROUTES } from '@/utils/constants';
import { useAuth } from '@/contexts/AuthContext';
export default function Footer() {
const currentYear = new Date().getFullYear();
const { isAuthenticated } = useAuth();
return (
<footer className="bg-gradient-to-br from-[#f5f5f0] via-[#faf9f5] to-[#f5f5f0] text-[#5a5a4e] border-t-2 border-[#e5e4dc]">
@ -74,20 +76,12 @@ export default function Footer() {
</li>
<li>
<Link
href={ROUTES.GAME}
href={isAuthenticated ? ROUTES.GAME : ROUTES.REGISTER}
className="text-sm text-[#8a8a7a] hover:text-[#d4a574] transition-colors"
>
Participer au jeu
</Link>
</li>
<li>
<Link
href="/about"
className="text-sm text-[#8a8a7a] hover:text-[#d4a574] transition-colors"
>
À propos
</Link>
</li>
<li>
<Link
href="/contact"
@ -135,14 +129,6 @@ export default function Footer() {
Règlement du jeu
</Link>
</li>
<li>
<Link
href="/cookies"
className="text-sm text-[#8a8a7a] hover:text-[#d4a574] transition-colors"
>
Gestion des cookies
</Link>
</li>
<li>
<Link
href="/legal"

View File

@ -90,7 +90,7 @@ export default function Header() {
{isAuthenticated ? (
<Link
href={ROUTES.GAME}
className="bg-[#d4a574] hover:bg-[#c4956a] text-white font-bold px-6 py-2 rounded-lg transition-all hover:shadow-xl"
className="bg-gradient-to-r from-[#d4a574] to-[#c4956a] hover:from-[#e5b685] hover:to-[#d4a574] text-white font-bold px-6 py-2 rounded-lg transition-all duration-300 hover:shadow-[0_0_20px_rgba(212,165,116,0.6)] hover:scale-105 shadow-lg"
>
Participer
</Link>
@ -98,11 +98,11 @@ export default function Header() {
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsParticiperDropdownOpen(!isParticiperDropdownOpen)}
className="bg-[#d4a574] hover:bg-[#c4956a] text-white font-bold px-6 py-2 rounded-lg transition-all hover:shadow-xl flex items-center gap-2"
className="bg-gradient-to-r from-[#d4a574] to-[#c4956a] hover:from-[#e5b685] hover:to-[#d4a574] text-white font-bold px-6 py-2 rounded-lg transition-all duration-300 hover:shadow-[0_0_20px_rgba(212,165,116,0.6)] hover:scale-105 shadow-lg flex items-center gap-2"
>
Participer
<svg
className={`w-4 h-4 transition-transform ${isParticiperDropdownOpen ? 'rotate-180' : ''}`}
className={`w-4 h-4 transition-transform duration-300 ${isParticiperDropdownOpen ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@ -112,22 +112,22 @@ export default function Header() {
</button>
{isParticiperDropdownOpen && (
<div className="absolute right-0 mt-2 w-56 bg-white rounded-lg shadow-xl border border-gray-200 py-2 z-50 animate-fadeIn">
<div className="absolute right-0 mt-2 w-56 bg-white rounded-xl shadow-2xl border-2 border-[#d4a574]/30 py-2 z-50 animate-fadeIn overflow-hidden">
<Link
href={ROUTES.LOGIN}
onClick={() => setIsParticiperDropdownOpen(false)}
className="block px-4 py-3 text-gray-700 hover:bg-green-50 hover:text-green-700 transition-colors"
className="block px-4 py-3 text-gray-700 hover:bg-gradient-to-r hover:from-[#d4a574]/10 hover:to-[#c4956a]/10 hover:text-[#c4956a] transition-all duration-300 hover:pl-6 hover:shadow-inner"
>
<span className="font-medium block">Connexion</span>
<span className="font-bold block">Connexion</span>
<p className="text-xs text-gray-500 mt-0.5">J'ai déjà un compte</p>
</Link>
<div className="border-t border-gray-100 my-1"></div>
<div className="border-t border-[#d4a574]/20 my-1"></div>
<Link
href={ROUTES.REGISTER}
onClick={() => setIsParticiperDropdownOpen(false)}
className="block px-4 py-3 text-gray-700 hover:bg-green-50 hover:text-green-700 transition-colors"
className="block px-4 py-3 text-gray-700 hover:bg-gradient-to-r hover:from-[#d4a574]/10 hover:to-[#c4956a]/10 hover:text-[#c4956a] transition-all duration-300 hover:pl-6 hover:shadow-inner"
>
<span className="font-medium block">Inscription</span>
<span className="font-bold block">Inscription</span>
<p className="text-xs text-gray-500 mt-0.5">Créer un nouveau compte</p>
</Link>
</div>

View File

@ -7,7 +7,8 @@
"build": "next build",
"start": "next start",
"lint": "eslint .",
"test": "echo \"No tests configured\" && exit 0"
"test": "echo \"No tests configured\" && exit 0",
"sonar": "sonar-scanner"
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",

33
sonar-project.properties Normal file
View File

@ -0,0 +1,33 @@
# Informations du projet
sonar.projectKey=the-tip-top-frontend
sonar.projectName=Thé Tip Top - Frontend
sonar.projectVersion=1.0.0
# Chemin des sources
sonar.sources=app,components,contexts,hooks,lib,services,types,utils
sonar.tests=__tests__
# Exclusions
sonar.exclusions=**/node_modules/**,**/*.spec.ts,**/*.test.ts,**/*.spec.tsx,**/*.test.tsx,**/coverage/**,**/.next/**,**/public/**,**/*.config.js,**/*.config.ts,**/dist/**,**/build/**
# Encodage des fichiers
sonar.sourceEncoding=UTF-8
# Langage du projet
sonar.language=ts
# Chemins de couverture de code (si tests configurés)
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.typescript.lcov.reportPaths=coverage/lcov.info
# Paramètres TypeScript
sonar.typescript.tsconfigPath=tsconfig.json
# Niveau de logs
sonar.log.level=INFO
# URL du serveur SonarQube (à adapter selon votre configuration)
# sonar.host.url=http://localhost:9000
# Token d'authentification (à configurer via variable d'environnement)
# sonar.login=${SONAR_TOKEN}