optimize archi
Some checks failed
Build and Test / Classic Build (pull_request) Failing after 2m19s
Build and Test / Docker Build (pull_request) Has been skipped

This commit is contained in:
Sacha VAUDEY 2025-09-14 12:54:18 +02:00
parent 8b374cf8c4
commit 57f5807876
28 changed files with 1420 additions and 902 deletions

View File

@ -1,31 +1,78 @@
name: Build
run-name: CI/CD website
name: Build and Test
run-name: Website build validation
on:
#push:
# branches:
# - main
# - dev
push:
branches: [main, dev]
pull_request:
types: [opened, synchronize, reopened]
branches:
- main
- dev
branches: [main, dev]
jobs:
build-check:
build-classic:
runs-on: ubuntu-latest
name: Classic Build
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '24.x'
- name: Install dependencies
node-version: '20'
- uses: pnpm/action-setup@v4
with:
version: 8
- name: Install and build
run: |
cd banquise-website
npm ci
- name: Building
pnpm install --frozen-lockfile
pnpm build
- name: Lint check
run: |
cd banquise-website
npm run build
pnpm lint
build-docker:
runs-on: ubuntu-latest
name: Docker Build
needs: build-classic
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Create Dockerfile if missing
run: |
cd banquise-website
if [ ! -f "Dockerfile" ]; then
cat > Dockerfile << 'EOF'
FROM node:20-alpine AS builder
RUN npm install -g pnpm
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM node:20-alpine AS runner
RUN npm install -g pnpm
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --prod
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["pnpm", "start"]
EOF
fi
- name: Build and test Docker image
run: |
cd banquise-website
docker build -t banquise-website:test .
docker run -d --name test-container -p 3000:3000 banquise-website:test
sleep 5
docker ps | grep test-container
docker stop test-container
docker rm test-container

View File

@ -1,18 +0,0 @@
node_modules
.next
dist
build
pnpm-debug.log
.env
.env.local
.env.development
.git
*.log
.DS_Store
.vscode
.idea
README.md
.gitignore
.eslintrc.json
*.sh
shell.nix

View File

@ -1,3 +0,0 @@
{
"extends": ["next/core-web-vitals"]
}

View File

@ -1,3 +1,159 @@
# Dependencies
node_modules/
.pnp
.pnp.js
# Testing
coverage/
.nyc_output
# Next.js
.next/
out/
build/
dist/
# Production builds
*.tgz
*.tar.gz
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons
build/Release
# Dependency directories
jspm_packages/
# Snowpack dependency directory
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
public
# Vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS)
.DS_Store
# Windows
Thumbs.db
ehthumbs.db
Desktop.ini
# VS Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
# ESLint cache
.eslintcache
# Prettier cache
.prettiercache
# Stylelint cache
.stylelintcache
# Logs
logs
*.log
@ -7,18 +163,79 @@ yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Diagnostic reports
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# Dependency directories
node_modules/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Temporary folders
tmp/
temp/
# Editor backup files
*~
*.swp
*.swo
# OS generated files
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Local environment files
.env*.local

View File

@ -1,9 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Minimal test CSS */
body {
margin: 0;
padding: 0;
}

View File

@ -1,13 +1,14 @@
@import "tailwindcss";
/* Configuration Tailwind v4 via CSS custom properties */
/* Configuration Tailwind v4 via CSS custom properties - Variables globales */
@layer base {
:root {
/* Polices */
--font-heading: 'Dela Gothic One', sans-serif;
--font-body: 'Roboto', sans-serif;
/* Couleurs personnalisées La Banquise */
--color-banquise-blue: 64, 180, 255; /* RGB for reuse in rgba() */
--color-banquise-blue: 64, 180, 255;
--color-banquise-blue-hex: #40B4FF;
--color-banquise-blue-dark: 31, 93, 137;
--color-banquise-blue-dark-hex: #1F5D89;
@ -16,27 +17,17 @@
--color-banquise-blue-lightest: 165, 240, 255;
--color-banquise-blue-lightest-hex: #A5F0FF;
--color-banquise-gray: #F6F6F6;
/* Transitions communes */
--transition-default: all 0.3s ease-in-out;
--transition-fast: all 0.2s ease-in-out;
/* Spacing commun */
--spacing-navbar: 4rem;
}
}
/* Variables CSS pour les polices et couleurs personnalisées */
:root {
--font-heading: 'Dela Gothic One', sans-serif;
--font-body: 'Roboto', sans-serif;
/* Couleurs personnalisées La Banquise */
--color-banquise-blue: 64, 180, 255; /* RGB for reuse in rgba() */
--color-banquise-blue-hex: #40B4FF;
--color-banquise-blue-dark: 31, 93, 137;
--color-banquise-blue-dark-hex: #1F5D89;
--color-banquise-blue-light: 105, 183, 226;
--color-banquise-blue-light-hex: #69B7E2;
--color-banquise-blue-lightest: 165, 240, 255;
--color-banquise-blue-lightest-hex: #A5F0FF;
--color-banquise-gray: #F6F6F6;
}
/* Minimal, valid utility helpers that rely on CSS variables and rgba() */
/* Minimal, valid utility helpers avec variables optimisées */
@layer utilities {
/* Text colors */
.text-banquise-blue { color: var(--color-banquise-blue-hex); }
@ -52,7 +43,7 @@
.bg-banquise-blue-lightest { background-color: var(--color-banquise-blue-lightest-hex); }
.bg-banquise-gray { background-color: var(--color-banquise-gray); }
/* Opacity helpers using rgba() and the stored RGB variables */
/* Opacity helpers using rgba() */
.bg-banquise-blue-5 { background-color: rgba(var(--color-banquise-blue), 0.05); }
.bg-banquise-blue-10 { background-color: rgba(var(--color-banquise-blue), 0.10); }
.bg-banquise-blue-20 { background-color: rgba(var(--color-banquise-blue), 0.20); }
@ -62,28 +53,35 @@
.border-banquise-blue { border-color: var(--color-banquise-blue-hex); }
.border-banquise-blue-lightest-30 { border-color: rgba(var(--color-banquise-blue-lightest), 0.3); }
/* Gradients shortcuts (use with existing Tailwind gradient utilities) */
/* Gradients shortcuts */
.from-banquise-blue { --tw-gradient-from: var(--color-banquise-blue-hex); }
.from-banquise-blue-dark { --tw-gradient-from: var(--color-banquise-blue-dark-hex); }
.via-banquise-blue { --tw-gradient-via: var(--color-banquise-blue-hex); }
.to-banquise-blue { --tw-gradient-to: var(--color-banquise-blue-hex); }
/* Simple shadow helpers */
/* Shadow helpers */
.shadow-banquise-blue-20 { box-shadow: 0 4px 6px -1px rgba(var(--color-banquise-blue), 0.20); }
/* Transitions communes */
.transition-default { transition: var(--transition-default); }
.transition-fast { transition: var(--transition-fast); }
}
/* Animations kept as valid keyframes */
/* Animations optimisées */
@keyframes gentle-float {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-15px) rotate(1deg); }
}
.animate-gentle-float { animation: gentle-float 6s ease-in-out infinite; }
@keyframes bounce-up {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
@keyframes bounce-up { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }
.animate-gentle-float { animation: gentle-float 6s ease-in-out infinite; }
.scroll-to-top:hover { animation: bounce-up 0.6s ease-in-out; }
/* Accessibility: respect reduced motion */
/* Configuration globale optimisée */
@media (prefers-reduced-motion: reduce) {
.animate-gentle-float,
.animate-ping,
@ -92,13 +90,29 @@
}
}
/* Global improvements */
html { scroll-behavior: smooth; }
body { overflow-x: hidden; }
/* Scrollbar styles for popup content */
.popup-content { scrollbar-width: thin; scrollbar-color: rgba(31,93,137,0.3) transparent; }
/* Scrollbar unifié pour tous les éléments */
.custom-scrollbar,
.popup-content {
scrollbar-width: thin;
scrollbar-color: rgba(31,93,137,0.3) transparent;
}
.custom-scrollbar::-webkit-scrollbar,
.popup-content::-webkit-scrollbar { width: 6px; }
.custom-scrollbar::-webkit-scrollbar-track,
.popup-content::-webkit-scrollbar-track { background: transparent; }
.popup-content::-webkit-scrollbar-thumb { background: rgba(31,93,137,0.3); border-radius: 3px; }
.popup-content::-webkit-scrollbar-thumb:hover { background: rgba(31,93,137,0.5); }
.custom-scrollbar::-webkit-scrollbar-thumb,
.popup-content::-webkit-scrollbar-thumb {
background: rgba(31,93,137,0.3);
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover,
.popup-content::-webkit-scrollbar-thumb:hover {
background: rgba(31,93,137,0.5);
}

View File

@ -1,135 +0,0 @@
import Link from 'next/link';
export default function TestColors() {
return (
<div className="min-h-screen bg-banquise-slate-50">
{/* Header avec gradient moderne */}
<div className="bg-gradient-banquise-nav text-white p-8 mb-8">
<h1 className="text-4xl font-bold mb-4">Nouvelle Palette Banquise</h1>
<p className="text-banquise-blue-200 text-lg">Design professionnel et harmonieux</p>
</div>
<div className="max-w-6xl mx-auto p-8 space-y-12">
{/* Palette Bleus */}
<section>
<h2 className="text-2xl font-bold text-banquise-blue-900 mb-6">Palette Bleus Principaux</h2>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
<div className="bg-banquise-blue-900 text-white p-4 rounded-lg text-center">
<div className="font-bold">900</div>
<div className="text-sm">#0f2a3d</div>
<div className="text-xs mt-2">Headers foncés</div>
</div>
<div className="bg-banquise-blue-800 text-white p-4 rounded-lg text-center">
<div className="font-bold">800</div>
<div className="text-sm">#1f5078</div>
<div className="text-xs mt-2">Navigation</div>
</div>
<div className="bg-banquise-blue-600 text-white p-4 rounded-lg text-center">
<div className="font-bold">600</div>
<div className="text-sm">#34a6fc</div>
<div className="text-xs mt-2">Boutons primaires</div>
</div>
<div className="bg-banquise-blue-400 text-white p-4 rounded-lg text-center">
<div className="font-bold">400</div>
<div className="text-sm">#76beee</div>
<div className="text-xs mt-2">Liens</div>
</div>
<div className="bg-banquise-blue-200 text-banquise-blue-900 p-4 rounded-lg text-center">
<div className="font-bold">200</div>
<div className="text-sm">#a0ecf9</div>
<div className="text-xs mt-2">Accents clairs</div>
</div>
</div>
</section>
{/* Palette Gris */}
<section>
<h2 className="text-2xl font-bold text-banquise-blue-900 mb-6">Palette Gris Harmonieux</h2>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
<div className="bg-banquise-slate-900 text-white p-4 rounded-lg text-center">
<div className="font-bold">Slate 900</div>
<div className="text-sm">#0f172a</div>
<div className="text-xs mt-2">Texte principal</div>
</div>
<div className="bg-banquise-slate-700 text-white p-4 rounded-lg text-center">
<div className="font-bold">Slate 700</div>
<div className="text-sm">#334155</div>
<div className="text-xs mt-2">Texte secondaire</div>
</div>
<div className="bg-banquise-slate-300 text-banquise-slate-800 p-4 rounded-lg text-center">
<div className="font-bold">Slate 300</div>
<div className="text-sm">#cbd5e1</div>
<div className="text-xs mt-2">Bordures</div>
</div>
<div className="bg-banquise-slate-100 text-banquise-slate-800 p-4 rounded-lg text-center">
<div className="font-bold">Slate 100</div>
<div className="text-sm">#f1f5f9</div>
<div className="text-xs mt-2">Fonds clairs</div>
</div>
<div className="bg-banquise-slate-50 text-banquise-slate-800 p-4 rounded-lg text-center border">
<div className="font-bold">Slate 50</div>
<div className="text-sm">#f8fafc</div>
<div className="text-xs mt-2">Fonds de page</div>
</div>
</div>
</section>
{/* Exemples d'application */}
<section>
<h2 className="text-2xl font-bold text-banquise-blue-900 mb-6">Exemples d&apos;Application</h2>
<div className="grid md:grid-cols-2 gap-6">
{/* Card moderne */}
<div className="bg-white border border-banquise-slate-200 rounded-xl p-6 shadow-banquise">
<div className="w-12 h-12 bg-gradient-banquise-primary rounded-lg mb-4 flex items-center justify-center text-white font-bold">
B
</div>
<h3 className="text-lg font-bold text-banquise-blue-900 mb-2">Service Moderne</h3>
<p className="text-banquise-slate-600 mb-4">Description avec contraste optimal pour la lisibilité.</p>
<button className="bg-gradient-banquise-primary text-white px-4 py-2 rounded-lg hover:shadow-banquise-lg transition-all">
Découvrir
</button>
</div>
{/* Card subtile */}
<div className="bg-gradient-banquise-card border border-banquise-blue-200 rounded-xl p-6">
<div className="w-12 h-12 bg-banquise-blue-200 rounded-lg mb-4 flex items-center justify-center text-banquise-blue-800 font-bold">
S
</div>
<h3 className="text-lg font-bold text-banquise-blue-800 mb-2">Service Subtil</h3>
<p className="text-banquise-slate-700 mb-4">Design épuré avec couleurs harmonieuses.</p>
<button className="bg-banquise-blue-600 text-white px-4 py-2 rounded-lg hover:bg-banquise-blue-700 transition-colors">
En savoir plus
</button>
</div>
</div>
</section>
{/* Navigation exemple */}
<section>
<h2 className="text-2xl font-bold text-banquise-blue-900 mb-6">Exemple Navigation</h2>
<div className="bg-gradient-banquise-nav rounded-xl p-4">
<div className="flex items-center justify-between">
<div className="text-white font-bold text-xl">La Banquise</div>
<div className="flex space-x-6">
<a href="#" className="text-banquise-blue-200 hover:text-white transition-colors">Accueil</a>
<a href="#" className="text-banquise-blue-200 hover:text-white transition-colors">Services</a>
<a href="#" className="text-banquise-blue-200 hover:text-white transition-colors">À propos</a>
</div>
<button className="bg-banquise-blue-600 text-white px-4 py-2 rounded-lg hover:bg-banquise-blue-500 transition-colors">
Contact
</button>
</div>
</div>
</section>
<div className="text-center pt-8">
<Link href="/" className="text-banquise-blue-600 hover:text-banquise-blue-800 underline text-lg">
Voir le site avec la nouvelle palette
</Link>
</div>
</div>
</div>
);
}

View File

@ -1,55 +0,0 @@
"use client"
export default function TestTailwind() {
return (
<div className="min-h-screen bg-gray-100 p-8">
<div className="max-w-2xl mx-auto">
<h1 className="text-4xl font-bold text-blue-600 mb-8">Test Tailwind CSS v4</h1>
<div className="space-y-6">
{/* Test des couleurs de base */}
<div className="bg-white p-6 rounded-lg shadow-lg">
<h2 className="text-2xl font-semibold mb-4">Couleurs de base</h2>
<div className="grid grid-cols-4 gap-4">
<div className="h-16 bg-red-500 rounded flex items-center justify-center text-white">Red</div>
<div className="h-16 bg-blue-500 rounded flex items-center justify-center text-white">Blue</div>
<div className="h-16 bg-green-500 rounded flex items-center justify-center text-white">Green</div>
<div className="h-16 bg-yellow-500 rounded flex items-center justify-center text-black">Yellow</div>
</div>
</div>
{/* Test des espacements */}
<div className="bg-white p-6 rounded-lg shadow-lg">
<h2 className="text-2xl font-semibold mb-4">Espacements</h2>
<div className="space-y-2">
<div className="p-2 bg-gray-200">p-2</div>
<div className="p-4 bg-gray-300">p-4</div>
<div className="p-6 bg-gray-400">p-6</div>
</div>
</div>
{/* Test des couleurs personnalisées */}
<div className="bg-white p-6 rounded-lg shadow-lg">
<h2 className="text-2xl font-semibold mb-4">Couleurs personnalisées Banquise</h2>
<div className="space-y-2">
<div className="text-banquise-blue p-2">Texte banquise-blue</div>
<div className="bg-banquise-blue text-white p-2 rounded">Background banquise-blue</div>
<div className="text-banquise-blue-dark p-2">Texte banquise-blue-dark</div>
<div className="bg-banquise-blue-dark text-white p-2 rounded">Background banquise-blue-dark</div>
</div>
</div>
{/* Test des animations */}
<div className="bg-white p-6 rounded-lg shadow-lg">
<h2 className="text-2xl font-semibold mb-4">Animations</h2>
<div className="space-y-4">
<div className="animate-pulse bg-blue-200 p-4 rounded">Pulse animation</div>
<div className="animate-bounce bg-green-200 p-4 rounded inline-block">Bounce animation</div>
<div className="animate-gentle-float bg-yellow-200 p-4 rounded inline-block">Custom gentle float</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,4 +1,5 @@
import React from 'react';
import { cn, commonClasses } from '@/lib/utils';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'discord' | 'auth' | 'secondary' | 'ghost' | 'outline';
@ -10,11 +11,12 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
}
// Factorisation des classes de taille et de variante
const sizeClasses = {
sm: 'px-4 py-2 text-sm',
md: 'px-6 py-3 text-base',
lg: 'px-8 py-4 text-lg',
};
} as const;
const variantClasses = {
primary: 'bg-gradient-to-r from-blue-600 to-blue-500 text-white shadow-lg hover:shadow-xl hover:from-blue-700 hover:to-blue-600 border-2 border-blue-600/20',
@ -23,7 +25,15 @@ const variantClasses = {
secondary: 'bg-white text-blue-700 border-2 border-blue-600 shadow-md hover:shadow-lg hover:bg-blue-50',
outline: 'bg-transparent text-gray-700 border-2 border-gray-300 hover:bg-gray-50 hover:border-gray-400',
ghost: 'bg-transparent text-gray-700 hover:bg-gray-100',
};
} as const;
// Composant loading spinner réutilisable
const LoadingSpinner = () => (
<svg className="animate-spin -ml-1 mr-3 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
);
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
@ -37,28 +47,25 @@ export const Button: React.FC<ButtonProps> = ({
disabled,
...props
}) => {
const baseClasses = [
'inline-flex items-center justify-center font-semibold rounded-xl transition-all duration-300 transform',
'hover:scale-105 active:scale-95 focus:outline-none focus:ring-4 focus:ring-blue-300',
sizeClasses[size],
variantClasses[variant],
fullWidth ? 'w-full' : '',
(disabled || loading) ? 'opacity-50 cursor-not-allowed transform-none' : '',
className
].filter(Boolean).join(' ');
const isDisabled = disabled || loading;
return (
<button
className={baseClasses}
disabled={disabled || loading}
className={cn(
commonClasses.buttonBase,
commonClasses.transition,
commonClasses.hoverScale,
'focus:ring-4 focus:ring-blue-300',
sizeClasses[size],
variantClasses[variant],
fullWidth && 'w-full',
isDisabled && 'opacity-50 cursor-not-allowed transform-none',
className
)}
disabled={isDisabled}
{...props}
>
{loading && (
<svg className="animate-spin -ml-1 mr-3 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
)}
{loading && <LoadingSpinner />}
{leftIcon && !loading && <span className="mr-2">{leftIcon}</span>}
{children}
{rightIcon && !loading && <span className="ml-2">{rightIcon}</span>}

View File

@ -1,5 +1,6 @@
import React from 'react';
import Image from 'next/image'
import { cn, commonClasses } from '@/lib/utils';
import type { Service } from '@/types/service';
interface ServiceCardProps {
@ -8,6 +9,17 @@ interface ServiceCardProps {
className?: string;
}
// Composant pour l'icône de flèche factorisant la structure répétitive
const HoverArrow = () => (
<div className="mt-6 flex justify-center opacity-0 group-hover:opacity-100 transition-all duration-300 transform translate-y-2 group-hover:translate-y-0">
<div className="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center shadow-lg">
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
);
export const ServiceCard: React.FC<ServiceCardProps> = ({
service,
onServiceClick,
@ -19,7 +31,15 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({
return (
<div
className={`group relative p-8 bg-white rounded-2xl shadow-lg hover:shadow-2xl border border-gray-200 hover:border-blue-300 transition-all duration-300 cursor-pointer transform hover:-translate-y-4 hover:scale-105 active:scale-95 ${className}`}
className={cn(
'group relative p-8 bg-white rounded-2xl',
'shadow-lg hover:shadow-2xl border border-gray-200 hover:border-blue-300',
commonClasses.transition,
'cursor-pointer transform hover:-translate-y-4',
commonClasses.hoverScale,
'active:scale-95',
className
)}
onClick={handleClick}
>
{/* Indicateur de survol */}
@ -28,34 +48,45 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({
{/* Contenu de la carte */}
<div className="relative z-10">
{/* Icône du service */}
<div className="mb-8 w-24 h-24 rounded-2xl bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center shadow-xl group-hover:shadow-2xl group-hover:scale-110 transition-all duration-300 mx-auto">
<div className={cn(
'mb-8 w-24 h-24 rounded-2xl mx-auto',
'bg-gradient-to-br from-blue-500 to-blue-600',
'flex items-center justify-center shadow-xl',
'group-hover:shadow-2xl group-hover:scale-110',
commonClasses.transition
)}>
<Image
src={service.image as any}
alt={service.icon}
className="h-12 w-12 transition-transform duration-300 group-hover:scale-110"
className={cn(
'h-12 w-12',
'transition-transform duration-300 group-hover:scale-110'
)}
width={48}
height={48}
/>
</div>
{/* Nom du service */}
<h3 className="text-xl md:text-2xl font-bold mb-4 text-gray-900 text-center group-hover:text-blue-700 transition-colors duration-300">
<h3 className={cn(
'text-xl md:text-2xl font-bold mb-4 text-gray-900 text-center',
'group-hover:text-blue-700',
commonClasses.transition
)}>
{service.name}
</h3>
{/* Description courte */}
<p className="text-gray-600 leading-relaxed text-center group-hover:text-gray-700 transition-colors duration-300">
<p className={cn(
'text-gray-600 leading-relaxed text-center',
'group-hover:text-gray-700',
commonClasses.transition
)}>
{service.description.split('.')[0]}.
</p>
{/* Flèche indicatrice au hover */}
<div className="mt-6 flex justify-center opacity-0 group-hover:opacity-100 transition-all duration-300 transform translate-y-2 group-hover:translate-y-0">
<div className="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center shadow-lg">
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
<HoverArrow />
</div>
</div>
);

View File

@ -2,91 +2,120 @@ import React from 'react';
import { URLS, SITE_CONFIG } from '@/lib/config/constants';
import { BookOpen, GitBranch, Gamepad2, Cloud, Rocket, Heart } from 'lucide-react';
import { useTranslation } from '@/lib/hooks/useTranslation';
import { cn, commonClasses } from '@/lib/utils';
// Factorisation des icônes SVG
const DiscordIcon = () => (
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.211.375-.445.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03z"/>
</svg>
);
const EmailIcon = () => (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
);
// Composant pour les liens de service factorisant la structure répétitive
interface ServiceLinkProps {
href: string;
icon: React.ComponentType<any>;
children: React.ReactNode;
}
const ServiceLink: React.FC<ServiceLinkProps> = ({ href, icon: Icon, children }) => (
<a
href={href}
className={cn(
'flex items-center text-gray-300 hover:text-blue-400',
commonClasses.transition,
'hover:translate-x-2 transform'
)}
>
<Icon className="w-5 h-5 mr-3" strokeWidth={2} />
{children}
</a>
);
// Composant pour les boutons sociaux factorisant la structure répétitive
interface SocialButtonProps {
href: string;
label: string;
children: React.ReactNode;
}
const SocialButton: React.FC<SocialButtonProps> = ({ href, label, children }) => (
<a
href={href}
className={cn(
'w-12 h-12 bg-gray-800 hover:bg-blue-600 rounded-xl',
'flex items-center justify-center',
commonClasses.transition,
commonClasses.hoverScale,
'shadow-lg hover:shadow-blue-500/25'
)}
aria-label={label}
>
{children}
</a>
);
export const Footer: React.FC = () => {
const { t } = useTranslation();
return (
<footer className="bg-gray-900 text-white py-16 px-4 sm:px-6 lg:px-8 border-t border-gray-800">
<div className="max-w-7xl mx-auto">
{/* Contenu principal du footer */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-12 mb-12">
{/* Marque et description */}
<div className="space-y-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center shadow-lg">
<span className="text-white font-bold text-xl">B</span>
<footer className="bg-gray-900 text-white py-16 px-4 sm:px-6 lg:px-8 border-t border-gray-800">
<div className="max-w-7xl mx-auto">
{/* Contenu principal du footer */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-12 mb-12">
{/* Marque et description */}
<div className="space-y-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center shadow-lg">
<span className="text-white font-bold text-xl">B</span>
</div>
<span className="text-white font-bold text-2xl">
{SITE_CONFIG.name}
</span>
</div>
<p className="text-gray-300 leading-relaxed">
{t.footer.description}
</p>
{/* Réseaux sociaux */}
<div className="flex items-center gap-4">
<SocialButton href={URLS.social.discord} label="Discord">
<DiscordIcon />
</SocialButton>
<SocialButton href={URLS.contact.email} label="Email">
<EmailIcon />
</SocialButton>
</div>
<span className="text-white font-bold text-2xl">
{SITE_CONFIG.name}
</span>
</div>
<p className="text-gray-300 leading-relaxed">
{t.footer.description}
</p>
{/* Réseaux sociaux */}
<div className="flex items-center gap-4">
<a
href={URLS.social.discord}
className="w-12 h-12 bg-gray-800 hover:bg-blue-600 rounded-xl flex items-center justify-center transition-all duration-300 hover:scale-110 shadow-lg hover:shadow-blue-500/25"
aria-label="Discord"
>
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.211.375-.445.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03z"/>
</svg>
</a>
<a
href={URLS.contact.email}
className="w-12 h-12 bg-gray-800 hover:bg-blue-600 rounded-xl flex items-center justify-center transition-all duration-300 hover:scale-110 shadow-lg hover:shadow-blue-500/25"
aria-label="Email"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</a>
</div>
</div>
{/* Liens rapides services */}
<div className="space-y-6">
<h3 className="text-white font-bold text-lg mb-6">{t.footer.ourServices}</h3>
<div className="space-y-4">
<a
href={URLS.services.wiki}
className="flex items-center text-gray-300 hover:text-blue-400 transition-colors duration-200 hover:translate-x-2 transform"
>
<BookOpen className="w-5 h-5 mr-3" strokeWidth={2} />
Wiki
</a>
<a
href={URLS.services.gitea}
className="flex items-center text-gray-300 hover:text-blue-400 transition-colors duration-200 hover:translate-x-2 transform"
>
<GitBranch className="w-5 h-5 mr-3" strokeWidth={2} />
Gitea
</a>
<a
href={URLS.services.panel}
className="flex items-center text-gray-300 hover:text-blue-400 transition-colors duration-200 hover:translate-x-2 transform"
>
<Gamepad2 className="w-5 h-5 mr-3" strokeWidth={2} />
{t.footer.gamingPanel}
</a>
<a
href={URLS.services.opencloud}
className="flex items-center text-gray-300 hover:text-blue-400 transition-colors duration-200 hover:translate-x-2 transform"
>
<Cloud className="w-5 h-5 mr-3" strokeWidth={2} />
OpenCloud
</a>
{/* Liens rapides services */}
<div className="space-y-6">
<h3 className="text-white font-bold text-lg mb-6">{t.footer.ourServices}</h3>
<div className="space-y-4">
<ServiceLink href={URLS.services.wiki} icon={BookOpen}>
Wiki
</ServiceLink>
<ServiceLink href={URLS.services.gitea} icon={GitBranch}>
Gitea
</ServiceLink>
<ServiceLink href={URLS.services.panel} icon={Gamepad2}>
{t.footer.gamingPanel}
</ServiceLink>
<ServiceLink href={URLS.services.opencloud} icon={Cloud}>
OpenCloud
</ServiceLink>
</div>
</div>
</div>
{/* Informations communauté */}
<div className="space-y-6">
<h3 className="text-white font-bold text-lg mb-6">{t.footer.community}</h3>
<div className="space-y-4">
{/* Informations communauté */}
<div className="space-y-6">
<h3 className="text-white font-bold text-lg mb-6">{t.footer.community}</h3>
<div className="bg-gradient-to-r from-blue-900/30 to-blue-800/30 rounded-xl p-6 border border-blue-800/30">
<h4 className="text-blue-400 font-semibold mb-2">{t.footer.joinAssociation}</h4>
<p className="text-gray-300 text-sm mb-4">
@ -94,7 +123,11 @@ export const Footer: React.FC = () => {
</p>
<a
href={URLS.social.discord}
className="inline-flex items-center text-blue-400 hover:text-blue-300 text-sm font-semibold transition-colors duration-200"
className={cn(
'inline-flex items-center text-blue-400 hover:text-blue-300',
'text-sm font-semibold',
commonClasses.transition
)}
>
<Rocket className="w-4 h-4 mr-2" strokeWidth={2} />
{t.footer.joinNow}
@ -102,24 +135,23 @@ export const Footer: React.FC = () => {
</div>
</div>
</div>
</div>
{/* Barre du bas */}
<div className="flex flex-col md:flex-row justify-between items-center gap-6 pt-8 border-t border-gray-800">
<p className="text-gray-400 text-sm text-center md:text-left">
© 2025 {SITE_CONFIG.name}. {t.footer.copyright}
</p>
<div className="flex items-center gap-6 text-sm text-gray-400">
<span className="flex items-center">
{t.footer.madeWith}
<Heart className="text-red-500 mx-1 w-4 h-4" strokeWidth={2} fill="currentColor" />
{t.footer.by}
</span>
<div className="w-1 h-1 bg-gray-600 rounded-full"></div>
<span className="text-blue-400 font-semibold">EPITA 2025</span>
{/* Barre du bas */}
<div className="flex flex-col md:flex-row justify-between items-center gap-6 pt-8 border-t border-gray-800">
<p className="text-gray-400 text-sm text-center md:text-left">
© 2025 {SITE_CONFIG.name}. {t.footer.copyright}
</p>
<div className="flex items-center gap-6 text-sm text-gray-400">
<span className="flex items-center">
{t.footer.madeWith}
<Heart className="text-red-500 mx-1 w-4 h-4" strokeWidth={2} fill="currentColor" />
{t.footer.by}
</span>
<div className="w-1 h-1 bg-gray-600 rounded-full"></div>
<span className="text-blue-400 font-semibold">EPITA 2025</span>
</div>
</div>
</div>
</div>
</footer>
</footer>
);
};

View File

@ -3,13 +3,9 @@ import { Button } from '@/components/common/Button';
import { Logo } from './navbar/Logo';
import { URLS } from '@/lib/config/constants';
import { useTranslation } from '@/lib/hooks/useTranslation';
import { cn, createNavClickHandler, commonClasses } from '@/lib/utils';
import type { Translation } from '@/types/i18n';
// Fonction utilitaire simple pour combiner les classes
const cn = (...classes: (string | undefined | null | false)[]): string => {
return classes.filter(Boolean).join(' ');
};
interface MobileMenuProps {
isOpen: boolean;
onClose: () => void;
@ -45,10 +41,8 @@ const MobileNavItem: React.FC<MobileNavItemProps> = ({
href={href}
onClick={handleClick}
className={cn(
'group flex items-center justify-between p-4 rounded-xl transition-all duration-300',
'bg-white/5 hover:bg-white/10 active:bg-white/15',
'border border-white/10 hover:border-white/20',
'hover:scale-[1.02] active:scale-[0.98]',
commonClasses.mobileMenuItem,
commonClasses.hoverScale,
'hover:shadow-lg hover:shadow-blue-500/20'
)}
target={isExternal ? '_blank' : undefined}
@ -89,40 +83,57 @@ const MobileNavItem: React.FC<MobileNavItemProps> = ({
export const MobileMenu: React.FC<MobileMenuProps> = ({ isOpen, onClose, translations }) => {
const { t } = useTranslation();
// Gérer le scroll du body
// Gérer le scroll du body - simplifié avec notre utilitaire
useEffect(() => {
const originalStyle = document.body.style.overflow;
if (isOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'unset';
document.body.style.overflow = originalStyle || 'unset';
}
return () => {
document.body.style.overflow = 'unset';
document.body.style.overflow = originalStyle || 'unset';
};
}, [isOpen]);
const handleNavClick = (sectionId: string) => {
if (sectionId === 'home') {
// Scroll to top for home section
window.scrollTo({
top: 0,
behavior: 'smooth'
});
} else if (sectionId === 'contact') {
// Open email client for contact
window.location.href = 'mailto:contact@la-banquise.fr';
} else {
// Scroll to specific section
const element = document.getElementById(sectionId);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
}
onClose();
// Gestionnaire de navigation optimisé
const handleNavClick = createNavClickHandler(onClose);
// Configuration des icônes SVG - factorisation
const icons = {
home: (
<svg className="w-5 h-5 text-blue-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
),
services: (
<svg className="w-5 h-5 text-blue-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
),
about: (
<svg className="w-5 h-5 text-blue-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
),
contact: (
<svg className="w-5 h-5 text-blue-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
),
discord: (
<svg className="w-5 h-5 text-[#5865F2]" fill="currentColor" viewBox="0 0 24 24">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.211.375-.445.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03z"/>
</svg>
),
user: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
)
};
return (
@ -156,10 +167,11 @@ export const MobileMenu: React.FC<MobileMenuProps> = ({ isOpen, onClose, transla
<button
className={cn(
'group relative p-3 rounded-xl transition-all duration-300',
'group relative p-3 rounded-xl',
commonClasses.transition,
'bg-white/10 hover:bg-white/20 active:bg-white/25',
'border border-white/20 hover:border-white/30',
'hover:scale-105 active:scale-95',
commonClasses.hoverScale,
'focus:outline-none focus:ring-2 focus:ring-blue-400/50'
)}
onClick={onClose}
@ -177,11 +189,7 @@ export const MobileMenu: React.FC<MobileMenuProps> = ({ isOpen, onClose, transla
{/* Section Navigation */}
<div className="space-y-3">
<MobileNavItem
icon={
<svg className="w-5 h-5 text-blue-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
}
icon={icons.home}
title={translations.home}
description={t.common.backToHome}
href="#home"
@ -189,11 +197,7 @@ export const MobileMenu: React.FC<MobileMenuProps> = ({ isOpen, onClose, transla
/>
<MobileNavItem
icon={
<svg className="w-5 h-5 text-blue-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
}
icon={icons.services}
title={translations.services}
description={t.common.discoverOffer}
href="#services"
@ -201,11 +205,7 @@ export const MobileMenu: React.FC<MobileMenuProps> = ({ isOpen, onClose, transla
/>
<MobileNavItem
icon={
<svg className="w-5 h-5 text-blue-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
}
icon={icons.about}
title={translations.about}
description={t.common.learnMoreAboutUs}
href="#about"
@ -213,11 +213,7 @@ export const MobileMenu: React.FC<MobileMenuProps> = ({ isOpen, onClose, transla
/>
<MobileNavItem
icon={
<svg className="w-5 h-5 text-blue-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
}
icon={icons.contact}
title={translations.contact}
description={t.common.sendEmail}
href="mailto:contact@la-banquise.fr"
@ -231,11 +227,7 @@ export const MobileMenu: React.FC<MobileMenuProps> = ({ isOpen, onClose, transla
{/* Social & External Links */}
<div className="space-y-3">
<MobileNavItem
icon={
<svg className="w-5 h-5 text-[#5865F2]" fill="currentColor" viewBox="0 0 24 24">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.211.375-.445.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03z"/>
</svg>
}
icon={icons.discord}
title="Discord"
description={t.common.joinCommunity}
href={URLS.social.discord}
@ -248,11 +240,7 @@ export const MobileMenu: React.FC<MobileMenuProps> = ({ isOpen, onClose, transla
<Button
variant="primary"
size="lg"
leftIcon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
}
leftIcon={icons.user}
onClick={() => {
window.open(URLS.services.auth, '_blank');
onClose();

View File

@ -5,13 +5,9 @@ import { NavLinks } from './navbar/NavLinks';
import { ActionButtons } from './navbar/ActionButtons';
import { MobileMenuButton } from './navbar/MobileMenuButton';
import { MobileMenu } from './MobileMenu';
import { cn, useResizeHandler } from '@/lib/utils';
import type { Translation } from '@/types/i18n';
// Fonction utilitaire simple pour combiner les classes
const mergeClasses = (...classes: (string | undefined | null | false)[]): string => {
return classes.filter(Boolean).join(' ');
};
interface ModernNavigationProps {
translations: Translation['navigation'];
languageSwitcher: React.ReactElement;
@ -24,68 +20,35 @@ export const ModernNavigation: React.FC<ModernNavigationProps> = ({
const { scrolled } = useScrollEffects();
const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);
// Fermer le menu mobile lors du redimensionnement
// Fermer le menu mobile lors du redimensionnement - optimisé
React.useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= 768) {
setMobileMenuOpen(false);
}
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
const cleanup = useResizeHandler(() => setMobileMenuOpen(false));
return cleanup;
}, []);
// Empêcher le scroll du body quand le menu mobile est ouvert
React.useEffect(() => {
if (mobileMenuOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'unset';
}
return () => {
document.body.style.overflow = 'unset';
};
}, [mobileMenuOpen]);
return (
<>
{/* Navigation moderne épurée */}
<nav className={mergeClasses(
// Position et z-index
<nav className={cn(
'fixed top-0 left-0 right-0 z-50',
// Style de fond moderne
'bg-blue-700/95 backdrop-blur-md border-b border-blue-600/30',
// Transition fluide
'transition-all duration-200 ease-in-out',
// Effet de scroll
'transition-fast',
scrolled && 'shadow-lg'
)}>
<div className="max-w-7xl mx-auto">
<div className="flex justify-between items-center px-4 sm:px-6 lg:px-8 h-16">
{/* Logo Section */}
<Logo scrolled={scrolled} />
{/* Navigation Links (Desktop) - Centré */}
<div className="flex-1 flex justify-center">
<NavLinks
translations={translations}
scrolled={scrolled}
/>
<NavLinks translations={translations} scrolled={scrolled} />
</div>
{/* Action Buttons (Desktop) */}
<ActionButtons
scrolled={scrolled}
languageSwitcher={languageSwitcher}
/>
{/* Mobile Menu Button */}
<MobileMenuButton
isOpen={mobileMenuOpen}
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
@ -100,7 +63,6 @@ export const ModernNavigation: React.FC<ModernNavigationProps> = ({
{/* Spacer pour compenser la navbar fixed */}
<div className="h-16" />
{/* Menu Mobile */}
<MobileMenu
isOpen={mobileMenuOpen}
onClose={() => setMobileMenuOpen(false)}

View File

@ -1,8 +1,12 @@
import React from 'react';
import { AccordionItem } from '@/components/ui/AccordionItem';
import { SectionHeader } from '@/components/ui/SectionHeader';
import { ServiceCardAbout } from '@/components/ui/ServiceCardAbout';
import { AccordionTitle } from '@/components/ui/AccordionTitle';
import { URLS } from '@/lib/config/constants';
import { Target, Settings, HelpCircle, Users, MessageCircle, Rocket, BookOpen, GitBranch, Gamepad2, Bird, Building, Mail, Cloud } from 'lucide-react';
import { useTranslation } from '@/lib/hooks/useTranslation';
import { cn, commonClasses } from '@/lib/utils';
interface AboutSectionProps {
openAccordion: string | null;
@ -15,18 +19,11 @@ export const AboutSection: React.FC<AboutSectionProps> = ({ openAccordion, toggl
return (
<section id="about" className="py-24 md:py-32 px-4 sm:px-6 lg:px-8 bg-white">
<div className="max-w-6xl mx-auto">
{/* Header de section moderne */}
<div className="text-center mb-20">
{/* Séparateur visuel */}
<div className="w-24 h-1.5 bg-gradient-to-r from-blue-600 to-blue-400 rounded-full mx-auto mb-8" />
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-gray-900 mb-6 leading-tight">
{t.about.title}
</h2>
<p className="text-lg md:text-xl text-gray-700 max-w-4xl mx-auto leading-relaxed font-medium">
{t.about.subtitle}
</p>
</div>
{/* Header de section moderne - factorisation */}
<SectionHeader
title={t.about.title}
subtitle={t.about.subtitle}
/>
{/* Section FAQ avec design moderne */}
<div className="space-y-8">
@ -38,14 +35,7 @@ export const AboutSection: React.FC<AboutSectionProps> = ({ openAccordion, toggl
</h3>
<AccordionItem
title={
<div className="flex items-center">
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center text-white mr-3">
<Target className="w-5 h-5" strokeWidth={2} />
</div>
{t.about.mission.title}
</div>
}
title={<AccordionTitle icon={Target} title={t.about.mission.title} />}
isOpen={openAccordion === "mission"}
onToggle={() => toggleAccordion("mission")}
>
@ -64,151 +54,118 @@ export const AboutSection: React.FC<AboutSectionProps> = ({ openAccordion, toggl
</div>
</AccordionItem>
<AccordionItem
title={
<div className="flex items-center">
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center text-white mr-3">
<Settings className="w-5 h-5" strokeWidth={2} />
<AccordionItem
title={<AccordionTitle icon={Settings} title={t.about.services.title} />}
isOpen={openAccordion === "services"}
onToggle={() => toggleAccordion("services")}
>
<div className="space-y-6 p-6 bg-gray-50 rounded-xl">
<div className="grid gap-6 md:grid-cols-2">
{/* Cartes de services avec design moderne - factorisation */}
<ServiceCardAbout
icon={BookOpen}
title={t.about.services.wiki.title}
description={t.about.services.wiki.description}
/>
<ServiceCardAbout
icon={GitBranch}
title={t.about.services.gitea.title}
description={t.about.services.gitea.description}
/>
<ServiceCardAbout
icon={Gamepad2}
title={t.about.services.panel.title}
description={t.about.services.panel.description}
/>
<ServiceCardAbout
icon={Bird}
title={t.about.services.pelican.title}
description={t.about.services.pelican.description}
/>
<ServiceCardAbout
icon={Building}
title={t.about.services.intranet.title}
description={t.about.services.intranet.description}
/>
<ServiceCardAbout
icon={Mail}
title={t.about.services.mails.title}
description={t.about.services.mails.description}
/>
<ServiceCardAbout
icon={Cloud}
title={t.about.services.opencloud.title}
description={t.about.services.opencloud.description}
colSpan={true}
/>
</div>
{t.about.services.title}
<p className="text-gray-600 mt-6 p-4 bg-blue-50 rounded-lg border border-blue-200">
<strong className="text-blue-800 flex items-center">
<Settings className="w-5 h-5 mr-2" strokeWidth={2} />
{t.about.services.title}
</strong> {t.about.services.note}
</p>
</div>
}
isOpen={openAccordion === "services"}
onToggle={() => toggleAccordion("services")}
>
<div className="space-y-6 p-6 bg-gray-50 rounded-xl">
<div className="grid gap-6 md:grid-cols-2">
{/* Cartes de services avec design moderne */}
<div className="flex items-start space-x-4 p-6 bg-white rounded-xl shadow-lg border border-gray-200 hover:shadow-xl transition-all duration-300">
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center text-white shadow-lg">
<BookOpen className="w-6 h-6" strokeWidth={2} />
</div>
<div>
<h4 className="font-bold text-gray-900 mb-2 text-lg">{t.about.services.wiki.title}</h4>
<p className="text-gray-600">{t.about.services.wiki.description}</p>
</div>
</div>
</AccordionItem>
<AccordionItem
title={<AccordionTitle icon={Users} title={t.about.community.title} />}
isOpen={openAccordion === "community"}
onToggle={() => toggleAccordion("community")}
>
<div className="space-y-8 p-6 bg-gray-50 rounded-xl">
<p className="text-gray-700 text-lg leading-relaxed">
{t.about.community.description}
</p>
<div className="flex items-start space-x-4 p-6 bg-white rounded-xl shadow-lg border border-gray-200 hover:shadow-xl transition-all duration-300">
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center text-white shadow-lg">
<GitBranch className="w-6 h-6" strokeWidth={2} />
</div>
<div>
<h4 className="font-bold text-gray-900 mb-2 text-lg">{t.about.services.gitea.title}</h4>
<p className="text-gray-600">{t.about.services.gitea.description}</p>
</div>
</div>
<div className="flex items-start space-x-4 p-6 bg-white rounded-xl shadow-lg border border-gray-200 hover:shadow-xl transition-all duration-300">
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center text-white shadow-lg">
<Gamepad2 className="w-6 h-6" strokeWidth={2} />
</div>
<div>
<h4 className="font-bold text-gray-900 mb-2 text-lg">{t.about.services.panel.title}</h4>
<p className="text-gray-600">{t.about.services.panel.description}</p>
</div>
</div>
<div className="flex items-start space-x-4 p-6 bg-white rounded-xl shadow-lg border border-gray-200 hover:shadow-xl transition-all duration-300">
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center text-white shadow-lg">
<Bird className="w-6 h-6" strokeWidth={2} />
</div>
<div>
<h4 className="font-bold text-gray-900 mb-2 text-lg">{t.about.services.pelican.title}</h4>
<p className="text-gray-600">{t.about.services.pelican.description}</p>
</div>
</div>
<div className="flex items-start space-x-4 p-6 bg-white rounded-xl shadow-lg border border-gray-200 hover:shadow-xl transition-all duration-300">
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center text-white shadow-lg">
<Building className="w-6 h-6" strokeWidth={2} />
</div>
<div>
<h4 className="font-bold text-gray-900 mb-2 text-lg">{t.about.services.intranet.title}</h4>
<p className="text-gray-600">{t.about.services.intranet.description}</p>
</div>
</div>
<div className="flex items-start space-x-4 p-6 bg-white rounded-xl shadow-lg border border-gray-200 hover:shadow-xl transition-all duration-300">
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center text-white shadow-lg">
<Mail className="w-6 h-6" strokeWidth={2} />
</div>
<div>
<h4 className="font-bold text-gray-900 mb-2 text-lg">{t.about.services.mails.title}</h4>
<p className="text-gray-600">{t.about.services.mails.description}</p>
</div>
</div>
<div className="flex items-start space-x-4 p-6 bg-white rounded-xl shadow-lg border border-gray-200 hover:shadow-xl transition-all duration-300 md:col-span-2">
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center text-white shadow-lg">
<Cloud className="w-6 h-6" strokeWidth={2} />
</div>
<div>
<h4 className="font-bold text-gray-900 mb-2 text-lg">{t.about.services.opencloud.title}</h4>
<p className="text-gray-600">{t.about.services.opencloud.description}</p>
</div>
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border-2 border-blue-200 rounded-2xl p-8">
<h4 className="font-bold text-gray-900 mb-6 flex items-center text-xl">
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center text-white mr-3">
<MessageCircle className="w-5 h-5" strokeWidth={2} />
</div>
{t.about.community.howToJoin}
</h4>
<ul className="space-y-4 text-gray-700 mb-8">
<li className="flex items-center text-lg">
<span className="w-2 h-2 bg-blue-500 rounded-full mr-4"></span>
{t.about.community.steps.step1}
</li>
<li className="flex items-center text-lg">
<span className="w-2 h-2 bg-blue-500 rounded-full mr-4"></span>
{t.about.community.steps.step2}
</li>
<li className="flex items-center text-lg">
<span className="w-2 h-2 bg-blue-500 rounded-full mr-4"></span>
{t.about.community.steps.step3}
</li>
</ul>
<a
href={URLS.social.discord}
className={cn(
'inline-flex items-center justify-center',
'px-8 py-4 text-lg font-bold text-white',
'bg-gradient-to-r from-blue-600 to-blue-500 rounded-xl',
'shadow-xl hover:shadow-2xl hover:from-blue-700 hover:to-blue-600',
commonClasses.transition,
commonClasses.hoverScale,
'border-2 border-blue-600/20'
)}
>
<Rocket className="w-6 h-6 mr-3" strokeWidth={2} />
{t.about.community.joinDiscord}
</a>
</div>
</div>
<p className="text-gray-600 mt-6 p-4 bg-blue-50 rounded-lg border border-blue-200">
<strong className="text-blue-800 flex items-center">
<Settings className="w-5 h-5 mr-2" strokeWidth={2} />
{t.about.services.title}
</strong> {t.about.services.note}
</p>
</div>
</AccordionItem>
<AccordionItem
title={
<div className="flex items-center">
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center text-white mr-3">
<Users className="w-5 h-5" strokeWidth={2} />
</div>
{t.about.community.title}
</div>
}
isOpen={openAccordion === "community"}
onToggle={() => toggleAccordion("community")}
>
<div className="space-y-8 p-6 bg-gray-50 rounded-xl">
<p className="text-gray-700 text-lg leading-relaxed">
{t.about.community.description}
</p>
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border-2 border-blue-200 rounded-2xl p-8">
<h4 className="font-bold text-gray-900 mb-6 flex items-center text-xl">
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center text-white mr-3">
<MessageCircle className="w-5 h-5" strokeWidth={2} />
</div>
{t.about.community.howToJoin}
</h4>
<ul className="space-y-4 text-gray-700 mb-8">
<li className="flex items-center text-lg">
<span className="w-2 h-2 bg-blue-500 rounded-full mr-4"></span>
{t.about.community.steps.step1}
</li>
<li className="flex items-center text-lg">
<span className="w-2 h-2 bg-blue-500 rounded-full mr-4"></span>
{t.about.community.steps.step2}
</li>
<li className="flex items-center text-lg">
<span className="w-2 h-2 bg-blue-500 rounded-full mr-4"></span>
{t.about.community.steps.step3}
</li>
</ul>
<a
href={URLS.social.discord}
className="inline-flex items-center justify-center px-8 py-4 text-lg font-bold text-white bg-gradient-to-r from-blue-600 to-blue-500 rounded-xl shadow-xl hover:shadow-2xl hover:from-blue-700 hover:to-blue-600 transition-all duration-300 transform hover:scale-105 border-2 border-blue-600/20"
>
<Rocket className="w-6 h-6 mr-3" strokeWidth={2} />
{t.about.community.joinDiscord}
</a>
</div>
</div>
</AccordionItem>
</AccordionItem>
</div>
</div>
</div>
</section>
</section>
);
};

View File

@ -1,6 +1,7 @@
import React from 'react';
import Image from 'next/image';
import { ArrowRight } from 'lucide-react';
import { cn, commonClasses } from '@/lib/utils';
import type { Translation } from '@/types/i18n';
interface HeroSectionProps {
@ -8,12 +9,69 @@ interface HeroSectionProps {
commonTranslations: Translation['common'];
}
// Composant pour les boutons CTA factorisant la structure répétitive
interface CTAButtonProps {
href: string;
primary?: boolean;
children: React.ReactNode;
icon?: React.ReactNode;
}
const CTAButton: React.FC<CTAButtonProps> = ({ href, primary = false, children, icon }) => {
const handleClick = (e: React.MouseEvent) => {
e.preventDefault();
const targetId = href.replace('#', '');
if (targetId === 'services' || targetId === 'about') {
document.getElementById(targetId)?.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
};
const baseClasses = cn(
'inline-flex items-center justify-center',
'text-lg font-bold rounded-2xl',
commonClasses.transition,
commonClasses.hoverScale,
'active:scale-95'
);
const primaryClasses = cn(
'px-12 py-5 text-white',
'bg-gradient-to-r from-blue-600 to-blue-500',
'shadow-2xl hover:shadow-blue-500/50',
'hover:-translate-y-2 border-2 border-blue-600/20',
'group relative overflow-hidden'
);
const secondaryClasses = cn(
'px-8 py-4 text-blue-700 bg-white',
'border-2 border-blue-600 shadow-lg',
'hover:shadow-xl hover:bg-blue-50'
);
return (
<a
href={href}
onClick={handleClick}
className={cn(baseClasses, primary ? primaryClasses : secondaryClasses)}
>
{primary && (
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 rounded-2xl" />
)}
<span className="relative z-10">{children}</span>
{icon && <span className="relative z-10 ml-3">{icon}</span>}
</a>
);
};
export const HeroSection: React.FC<HeroSectionProps> = ({ translations, commonTranslations }) => (
<section
id="home"
className="min-h-screen flex flex-col justify-center items-center text-center relative px-4 sm:px-6 lg:px-8 bg-gradient-to-br from-gray-50 via-blue-50/30 to-gray-100"
>
{/* Motif de fond avec grille plus visible */}
{/* Motifs de fond optimisés - factorisation des styles répétitifs */}
<div
className="absolute inset-0 opacity-40"
style={{
@ -21,8 +79,6 @@ export const HeroSection: React.FC<HeroSectionProps> = ({ translations, commonTr
backgroundSize: '32px 32px'
}}
/>
{/* Grille secondaire pour plus de profondeur */}
<div
className="absolute inset-0 opacity-20"
style={{
@ -37,17 +93,24 @@ export const HeroSection: React.FC<HeroSectionProps> = ({ translations, commonTr
{/* Logo principal avec effet moderne */}
<div className="mb-16 group">
<div className="relative inline-block">
{/* Effet de glow au hover */}
<div className="absolute inset-0 bg-blue-400/30 rounded-3xl blur-3xl opacity-0 group-hover:opacity-100 transition-all duration-700 scale-150" />
{/* Container du logo avec glassmorphism */}
<div className="relative bg-white/90 backdrop-blur-lg rounded-3xl p-10 border border-blue-200/50 shadow-2xl transition-all duration-500 group-hover:shadow-blue-200/50 group-hover:shadow-3xl group-hover:scale-105">
<div className={cn(
'relative bg-white/90 backdrop-blur-lg rounded-3xl p-10',
'border border-blue-200/50 shadow-2xl',
commonClasses.transition,
'group-hover:shadow-blue-200/50 group-hover:shadow-3xl',
commonClasses.hoverScale
)}>
<Image
src="/assets/banquise_server.svg"
alt={translations.title}
width={140}
height={140}
className="w-28 h-28 md:w-32 md:h-32 lg:w-36 lg:h-36 transition-transform duration-500 group-hover:scale-110"
className={cn(
'w-28 h-28 md:w-32 md:h-32 lg:w-36 lg:h-36',
'transition-transform duration-500 group-hover:scale-110'
)}
style={{
filter: 'drop-shadow(0 8px 24px rgba(59, 130, 246, 0.4))'
}}
@ -66,44 +129,19 @@ export const HeroSection: React.FC<HeroSectionProps> = ({ translations, commonTr
{translations.subtitle}
</p>
{/* Call-to-action super mis en valeur */}
{/* Call-to-action optimisé */}
<div className="flex flex-col sm:flex-row gap-6 justify-center items-center mb-20">
{/* Bouton principal très visible */}
<a
<CTAButton
href="#services"
onClick={(e) => {
e.preventDefault();
document.getElementById('services')?.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}}
className="group relative inline-flex items-center justify-center px-12 py-5 text-lg font-bold text-white bg-gradient-to-r from-blue-600 to-blue-500 rounded-2xl shadow-2xl hover:shadow-blue-500/50 transition-all duration-300 transform hover:scale-110 hover:-translate-y-2 active:scale-95 border-2 border-blue-600/20"
primary
icon={<ArrowRight className="w-6 h-6 transition-transform duration-300 group-hover:translate-x-2" strokeWidth={2.5} />}
>
{/* Effet de brillance au hover */}
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 rounded-2xl" />
<span className="relative z-10">{translations.cta}</span>
<ArrowRight
className="relative z-10 ml-3 w-6 h-6 transition-transform duration-300 group-hover:translate-x-2"
strokeWidth={2.5}
/>
</a>
{translations.cta}
</CTAButton>
{/* Bouton secondaire épuré */}
<a
href="#about"
onClick={(e) => {
e.preventDefault();
document.getElementById('about')?.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}}
className="inline-flex items-center justify-center px-8 py-4 text-lg font-semibold text-blue-700 bg-white border-2 border-blue-600 rounded-xl shadow-lg hover:shadow-xl hover:bg-blue-50 hover:scale-105 transition-all duration-300 active:scale-95"
>
<CTAButton href="#about">
{commonTranslations.learnMore}
</a>
</CTAButton>
</div>
</div>
</section>

View File

@ -1,5 +1,6 @@
import React from 'react';
import { ServiceCard } from '@/components/common/ServiceCard';
import { SectionHeader } from '@/components/ui/SectionHeader';
import { useTranslation } from '@/lib/hooks/useTranslation';
import type { Service } from '@/types/service';
@ -23,34 +24,24 @@ export const ServicesSection: React.FC<ServicesSectionProps> = ({
id="services"
className="py-24 md:py-32 px-4 sm:px-6 lg:px-8 bg-gradient-to-br from-white via-blue-50/30 to-gray-50"
>
<div className="max-w-7xl mx-auto">
{/* Header de section moderne avec forte hiérarchie */}
<div className="text-center mb-20">
{/* Séparateur visuel moderne */}
<div className="w-24 h-1.5 bg-gradient-to-r from-blue-600 to-blue-400 rounded-full mx-auto mb-8" />
<div className="max-w-7xl mx-auto">
{/* Header de section moderne avec forte hiérarchie - factorisation */}
<SectionHeader
title={t.sections.ourServices}
subtitle={translations.discoverFeatures}
/>
{/* Titre principal avec contraste fort */}
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-gray-900 mb-6 leading-tight">
{t.sections.ourServices}
</h2>
{/* Sous-titre avec bon contraste */}
<p className="text-lg md:text-xl text-gray-700 mx-auto max-w-3xl leading-relaxed font-medium">
{translations.discoverFeatures}
</p>
{/* Grille de services avec espacement généreux */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-12">
{services.map((service) => (
<ServiceCard
key={service.name}
service={service}
onServiceClick={onServiceClick}
/>
))}
</div>
</div>
{/* Grille de services avec espacement généreux */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-12">
{services.map((service) => (
<ServiceCard
key={service.name}
service={service}
onServiceClick={onServiceClick}
/>
))}
</div>
</div>
</section>
);
};

View File

@ -0,0 +1,17 @@
import React from 'react';
import { LucideIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
interface AccordionTitleProps {
icon: LucideIcon;
title: string;
}
export const AccordionTitle: React.FC<AccordionTitleProps> = ({ icon: Icon, title }) => (
<div className="flex items-center">
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center text-white mr-3">
<Icon className="w-5 h-5" strokeWidth={2} />
</div>
{title}
</div>
);

View File

@ -1,5 +1,7 @@
import React, { useEffect } from 'react';
import { URLS } from '@/lib/config/constants';
import { FeatureBadge, FeatureItem, SectionTitle } from './PopupComponents';
import { cn, commonClasses } from '@/lib/utils';
import type { Service } from '@/types/service';
import type { Translation } from '@/types/i18n';
import { ClipboardList, Zap, Check, Lock, Rocket } from 'lucide-react';
@ -13,10 +15,11 @@ interface PopupProps {
export const Popup: React.FC<PopupProps> = ({ service, onClose, translations }) => {
// Empêcher le scroll du body quand la popup est ouverte
useEffect(() => {
const originalStyle = document.body.style.overflow;
document.body.style.overflow = 'hidden';
return () => {
document.body.style.overflow = 'unset';
document.body.style.overflow = originalStyle || 'unset';
};
}, []);
@ -28,7 +31,14 @@ export const Popup: React.FC<PopupProps> = ({ service, onClose, translations })
<div className="absolute top-4 right-4 z-50">
<button
onClick={onClose}
className="bg-white/90 hover:bg-white border border-gray-300 text-xl cursor-pointer text-gray-700 flex items-center justify-center w-10 h-10 sm:w-12 sm:h-12 rounded-full transition-all duration-200 hover:scale-110 active:scale-95 shadow-lg backdrop-blur-sm"
className={cn(
'bg-white/90 hover:bg-white border border-gray-300',
'text-xl cursor-pointer text-gray-700',
'flex items-center justify-center w-10 h-10 sm:w-12 sm:h-12 rounded-full',
commonClasses.transition,
commonClasses.hoverScale,
'shadow-lg backdrop-blur-sm'
)}
aria-label={translations.close}
>
×
@ -62,53 +72,30 @@ export const Popup: React.FC<PopupProps> = ({ service, onClose, translations })
{/* Content - Forcer le fond blanc */}
<div className="p-6 sm:p-8 bg-white">
{/* Description */}
<h3 className="text-xl sm:text-2xl lg:text-3xl mb-4 lg:mb-6 text-gray-800 font-heading font-bold flex items-center">
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center text-white mr-3">
<ClipboardList className="w-5 h-5 sm:w-6 sm:h-6" strokeWidth={2} />
</div>
Description détaillée
</h3>
<SectionTitle icon={ClipboardList} title="Description détaillée" />
<div className="bg-gradient-to-br from-blue-50 to-blue-100/50 rounded-2xl p-4 lg:p-6 border border-blue-200 mb-8">
<p className="text-gray-700 leading-relaxed text-base sm:text-lg lg:text-xl mb-4">
{service.description}
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-6">
<div className="flex items-center p-3 bg-white/80 rounded-xl border border-gray-200 shadow-sm">
<div className="w-10 h-10 bg-gradient-to-br from-green-500 to-green-600 rounded-lg flex items-center justify-center text-white mr-3">
<Check className="w-5 h-5" strokeWidth={2} />
</div>
<div>
<div className="font-semibold text-gray-800 text-sm">99.9% Uptime</div>
<div className="text-gray-600 text-xs">Disponibilité garantie</div>
</div>
</div>
<div className="flex items-center p-3 bg-white/80 rounded-xl border border-gray-200 shadow-sm">
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center text-white mr-3">
<Lock className="w-5 h-5" strokeWidth={2} />
</div>
<div>
<div className="font-semibold text-gray-800 text-sm">Sécurisé</div>
<div className="text-gray-600 text-xs">SSL & Backups</div>
</div>
</div>
<FeatureBadge
icon={Check}
title="99.9% Uptime"
subtitle="Disponibilité garantie"
/>
<FeatureBadge
icon={Lock}
title="Sécurisé"
subtitle="SSL & Backups"
/>
</div>
</div>
{/* Fonctionnalités */}
<h3 className="text-xl sm:text-2xl lg:text-3xl mb-4 lg:mb-6 text-gray-800 font-heading font-bold flex items-center">
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center text-white mr-3">
<Zap className="w-5 h-5 sm:w-6 sm:h-6" strokeWidth={2} />
</div>
{translations.discoverFeatures}
</h3>
<SectionTitle icon={Zap} title={translations.discoverFeatures} />
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
{service.features.map((feature, index) => (
<div key={index} className="flex items-start bg-blue-50 rounded-xl p-4 border border-blue-200 hover:bg-blue-100 transition-colors duration-200 group">
<div className="w-6 h-6 bg-gradient-to-br from-blue-600 to-blue-500 rounded-full flex items-center justify-center mr-3 mt-0.5 flex-shrink-0 group-hover:scale-110 transition-transform duration-200">
<div className="w-2 h-2 bg-white rounded-full"></div>
</div>
<span className="text-gray-700 font-medium text-sm lg:text-base leading-relaxed">{feature}</span>
</div>
<FeatureItem key={index} feature={feature} index={index} />
))}
</div>
@ -118,14 +105,23 @@ export const Popup: React.FC<PopupProps> = ({ service, onClose, translations })
href={service.url}
target="_blank"
rel="noopener noreferrer"
className="w-full inline-flex items-center justify-center bg-gradient-to-r from-blue-600 to-blue-500 text-white border-0 py-4 px-6 sm:px-8 rounded-2xl cursor-pointer no-underline font-bold tracking-wide shadow-lg transition-all duration-300 hover:shadow-xl hover:-translate-y-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500 text-base lg:text-lg hover:scale-[1.02] active:scale-95"
className={cn(
'w-full inline-flex items-center justify-center',
'bg-gradient-to-r from-blue-600 to-blue-500 text-white',
'border-0 py-4 px-6 sm:px-8 rounded-2xl cursor-pointer no-underline',
'font-bold tracking-wide shadow-lg',
commonClasses.transition,
commonClasses.hoverLift,
'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500',
'text-base lg:text-lg hover:scale-[1.02] active:scale-95'
)}
>
<Rocket className="w-6 h-6 lg:w-7 lg:h-7 mr-3" strokeWidth={2} />
<span>Accéder à {service.name}</span>
</a>
<p className="text-center text-sm text-gray-500 mt-4">
Besoin d&apos;aide ? Rejoignez notre <a href={URLS.social.discord} className="text-blue-600 hover:text-blue-700 transition-colors duration-200 font-medium">Discord</a> pour obtenir du support
Besoin d&apos;aide ? Rejoignez notre <a href={URLS.social.discord} className={cn('text-blue-600 hover:text-blue-700 font-medium', commonClasses.transition)}>Discord</a> pour obtenir du support
</p>
</div>
</div>

View File

@ -0,0 +1,53 @@
import React from 'react';
import { LucideIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
interface FeatureBadgeProps {
icon: LucideIcon;
title: string;
subtitle: string;
}
export const FeatureBadge: React.FC<FeatureBadgeProps> = ({
icon: Icon,
title,
subtitle
}) => (
<div className="flex items-center p-3 bg-white/80 rounded-xl border border-gray-200 shadow-sm">
<div className="w-10 h-10 bg-gradient-to-br from-green-500 to-green-600 rounded-lg flex items-center justify-center text-white mr-3">
<Icon className="w-5 h-5" strokeWidth={2} />
</div>
<div>
<div className="font-semibold text-gray-800 text-sm">{title}</div>
<div className="text-gray-600 text-xs">{subtitle}</div>
</div>
</div>
);
interface FeatureItemProps {
feature: string;
index: number;
}
export const FeatureItem: React.FC<FeatureItemProps> = ({ feature, index }) => (
<div className="flex items-start bg-blue-50 rounded-xl p-4 border border-blue-200 hover:bg-blue-100 transition-colors duration-200 group">
<div className="w-6 h-6 bg-gradient-to-br from-blue-600 to-blue-500 rounded-full flex items-center justify-center mr-3 mt-0.5 flex-shrink-0 group-hover:scale-110 transition-transform duration-200">
<div className="w-2 h-2 bg-white rounded-full"></div>
</div>
<span className="text-gray-700 font-medium text-sm lg:text-base leading-relaxed">{feature}</span>
</div>
);
interface SectionTitleProps {
icon: LucideIcon;
title: string;
}
export const SectionTitle: React.FC<SectionTitleProps> = ({ icon: Icon, title }) => (
<h3 className="text-xl sm:text-2xl lg:text-3xl mb-4 lg:mb-6 text-gray-800 font-heading font-bold flex items-center">
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center text-white mr-3">
<Icon className="w-5 h-5 sm:w-6 sm:h-6" strokeWidth={2} />
</div>
{title}
</h3>
);

View File

@ -0,0 +1,31 @@
import React from 'react';
import { cn } from '@/lib/utils';
interface SectionHeaderProps {
title: string;
subtitle?: string;
className?: string;
}
export const SectionHeader: React.FC<SectionHeaderProps> = ({
title,
subtitle,
className
}) => (
<div className={cn('text-center mb-20', className)}>
{/* Séparateur visuel moderne - factorisation du design répétitif */}
<div className="w-24 h-1.5 bg-gradient-to-r from-blue-600 to-blue-400 rounded-full mx-auto mb-8" />
{/* Titre principal avec contraste fort */}
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-gray-900 mb-6 leading-tight">
{title}
</h2>
{/* Sous-titre avec bon contraste */}
{subtitle && (
<p className="text-lg md:text-xl text-gray-700 mx-auto max-w-3xl leading-relaxed font-medium">
{subtitle}
</p>
)}
</div>
);

View File

@ -0,0 +1,33 @@
import React from 'react';
import { LucideIcon } from 'lucide-react';
import { cn, commonClasses } from '@/lib/utils';
interface ServiceCardAboutProps {
icon: LucideIcon;
title: string;
description: string;
colSpan?: boolean;
}
export const ServiceCardAbout: React.FC<ServiceCardAboutProps> = ({
icon: Icon,
title,
description,
colSpan = false
}) => (
<div className={cn(
'flex items-start space-x-4 p-6 bg-white rounded-xl',
'shadow-lg border border-gray-200',
commonClasses.transition,
commonClasses.cardHover,
colSpan && 'md:col-span-2'
)}>
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center text-white shadow-lg">
<Icon className="w-6 h-6" strokeWidth={2} />
</div>
<div>
<h4 className="font-bold text-gray-900 mb-2 text-lg">{title}</h4>
<p className="text-gray-600">{description}</p>
</div>
</div>
);

View File

@ -2,7 +2,7 @@ import { useState, useCallback } from 'react';
/**
* Hook personnalisé pour gérer l'état des accordéons
* Remplace la logique dans App.tsx et simplifie la gestion d'état
* Optimisé avec des callbacks memoized et une API simplifiée
*/
export const useAccordion = (initialState: string | null = null) => {
const [openAccordion, setOpenAccordion] = useState<string | null>(initialState);

View File

@ -0,0 +1,118 @@
import { useEffect, useCallback, useState } from 'react';
/**
* Hook centralisé pour la gestion du scroll du body
* Factorisation de la logique répétée dans plusieurs composants
*/
export const useBodyScrollLock = (isLocked: boolean) => {
useEffect(() => {
const originalStyle = document.body.style.overflow;
if (isLocked) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = originalStyle || 'unset';
}
return () => {
document.body.style.overflow = originalStyle || 'unset';
};
}, [isLocked]);
};
/**
* Hook pour gérer les événements de redimensionnement avec debounce
*/
export const useResizeEvent = (callback: () => void, breakpoint: number = 768, delay: number = 100) => {
useEffect(() => {
let timeoutId: NodeJS.Timeout;
const handleResize = () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
if (window.innerWidth >= breakpoint) {
callback();
}
}, delay);
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
clearTimeout(timeoutId);
};
}, [callback, breakpoint, delay]);
};
/**
* Hook pour les états de toggle simples (booléens)
*/
export const useToggle = (initialState: boolean = false) => {
const [state, setState] = useState(initialState);
const toggle = useCallback(() => setState(prev => !prev), []);
const setTrue = useCallback(() => setState(true), []);
const setFalse = useCallback(() => setState(false), []);
return {
state,
toggle,
setTrue,
setFalse,
setState
};
};
/**
* Hook pour gérer les modales génériques
*/
export const useModal = <T = any>(initialState: T | null = null) => {
const [data, setData] = useState<T | null>(initialState);
const open = useCallback((newData: T) => {
setData(newData);
}, []);
const close = useCallback(() => {
setData(null);
}, []);
const isOpen = data !== null;
return {
data,
open,
close,
isOpen
};
};
/**
* Hook pour la navigation entre sections avec scroll smooth
*/
export const useNavigation = () => {
const scrollToSection = useCallback((sectionId: string) => {
if (sectionId === 'home') {
window.scrollTo({ top: 0, behavior: 'smooth' });
} else if (sectionId === 'contact') {
window.location.href = 'mailto:contact@la-banquise.fr';
} else {
const element = document.getElementById(sectionId);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
}, []);
const createNavHandler = useCallback((onClose?: () => void) => {
return (sectionId: string) => {
scrollToSection(sectionId);
onClose?.();
};
}, [scrollToSection]);
return {
scrollToSection,
createNavHandler
};
};

View File

@ -1,64 +0,0 @@
// DEPRECATED: This file is being replaced by designSystem.ts
// Please use the new design system for new components
// This file is kept for backward compatibility during migration
// Re-export the legacy commonStyles structure for backward compatibility
export const commonStyles = {
// Gradients - Keep existing structure
gradients: {
primary: "bg-gradient-to-r from-banquise-blue to-banquise-blue-light",
primaryBr: "bg-gradient-to-br from-banquise-blue to-banquise-blue-light",
card: "bg-gradient-to-br from-banquise-blue-dark/10 to-banquise-blue-dark/5",
cardHover: "hover:from-banquise-blue-dark/15 hover:to-banquise-blue-dark/8",
discord: "bg-gradient-to-r from-indigo-600 to-purple-600",
discordHover: "hover:from-indigo-500 hover:to-purple-500"
},
// Buttons - Keep existing structure
buttons: {
primary: "inline-flex items-center justify-center font-bold text-white border-0 rounded-2xl transition-all duration-300 hover:shadow-xl hover:-translate-y-1 hover:scale-105 active:scale-95",
discord: "group relative overflow-hidden px-4 lg:px-6 py-2.5 lg:py-3 text-white font-semibold text-sm lg:text-base rounded-xl transition-all duration-300 hover:shadow-xl hover:shadow-indigo-500/25 hover:-translate-y-1 hover:scale-105",
auth: "group relative overflow-hidden px-4 lg:px-6 py-2.5 lg:py-3 text-white font-semibold text-sm lg:text-base rounded-xl transition-all duration-300 hover:shadow-xl hover:-translate-y-1 hover:scale-105"
},
// Cards - Keep existing structure
cards: {
base: "backdrop-blur-lg rounded-2xl border border-banquise-blue-lightest/30 transition-all duration-300",
hover: "hover:shadow-xl hover:border-banquise-blue-lightest/50",
interactive: "cursor-pointer hover:-translate-y-4 hover:shadow-2xl active:scale-95"
},
// Text - Keep existing structure
text: {
heading: "font-heading font-bold tracking-tight",
headingXl: "text-3xl sm:text-4xl md:text-5xl text-banquise-gray font-heading font-bold tracking-tight",
headingLg: "text-2xl sm:text-3xl md:text-4xl text-banquise-gray font-heading font-bold tracking-tight",
headingMd: "text-xl sm:text-2xl md:text-3xl text-banquise-blue-dark font-heading font-bold tracking-tight",
headingSm: "text-lg sm:text-xl md:text-2xl text-banquise-blue-dark font-heading font-semibold tracking-tight",
subheading: "text-base sm:text-lg md:text-xl text-banquise-gray/90 font-medium leading-relaxed",
body: "text-sm sm:text-base md:text-lg text-banquise-blue-dark/90 leading-relaxed",
description: "text-banquise-gray/80 leading-relaxed",
muted: "text-banquise-gray/90 leading-relaxed",
lightHeading: "text-banquise-blue-lightest font-heading font-bold tracking-tight",
lightBody: "text-white/90 leading-relaxed"
},
// Layout - Keep existing structure
layout: {
section: "py-12 sm:py-16 md:py-20 w-full max-w-6xl mx-auto px-4 sm:px-6 md:px-8",
container: "max-w-6xl mx-auto",
divider: "w-20 h-1 bg-gradient-to-r from-banquise-blue-lightest to-banquise-blue mx-auto mb-6 sm:mb-8 rounded-full"
},
// Icons - Keep existing structure
icons: {
base: "w-16 h-16 sm:w-20 sm:h-20 lg:w-24 lg:h-24 rounded-2xl flex items-center justify-center text-3xl sm:text-4xl lg:text-5xl shadow-lg",
small: "w-10 h-10 rounded-lg flex items-center justify-center text-white"
},
// Navigation - Keep existing structure
nav: {
link: "px-4 lg:px-6 py-2.5 lg:py-3 text-white/90 hover:text-white font-medium text-sm lg:text-base rounded-xl transition-all duration-300 hover:bg-white/10 hover:backdrop-blur-sm relative group",
mobileItem: "group flex items-center p-4 text-white/90 hover:text-white no-underline rounded-2xl hover:bg-gradient-to-r hover:from-banquise-blue/20 hover:to-banquise-blue-light/20 transition-all duration-300 border border-transparent hover:border-banquise-blue-lightest/20"
}
} as const;

View File

@ -0,0 +1,197 @@
// Design System La Banquise - Système centralisé de styles
export const designSystem = {
// 🎨 Gradients
gradients: {
primary: "bg-gradient-to-r from-banquise-blue to-banquise-blue-light",
primaryBr: "bg-gradient-to-br from-banquise-blue to-banquise-blue-light",
card: "bg-gradient-to-br from-banquise-blue-dark/10 to-banquise-blue-dark/5",
cardHover: "hover:from-banquise-blue-dark/15 hover:to-banquise-blue-dark/8",
discord: "bg-gradient-to-r from-indigo-600 to-purple-600",
discordHover: "hover:from-indigo-500 hover:to-purple-500",
hero: "bg-gradient-to-br from-banquise-blue to-banquise-blue-dark",
section: "bg-gradient-to-b from-white/95 to-white"
},
// 🎯 Boutons optimisés
buttons: {
base: "inline-flex items-center justify-center font-bold border-0 rounded-2xl transition-all duration-300 active:scale-95",
effects: "hover:shadow-xl hover:-translate-y-1 hover:scale-105",
variants: {
primary: "text-white",
discord: "group relative overflow-hidden text-white font-semibold rounded-xl hover:shadow-indigo-500/25",
auth: "group relative overflow-hidden text-white font-semibold rounded-xl",
secondary: "bg-white/10 backdrop-blur-sm text-white border border-white/20",
outline: "bg-transparent border-2 border-banquise-blue text-banquise-blue hover:bg-banquise-blue hover:text-white"
},
sizes: {
sm: "px-3 py-2 text-sm",
md: "px-4 lg:px-6 py-2.5 lg:py-3 text-sm lg:text-base",
lg: "px-6 lg:px-8 py-3 lg:py-4 text-base lg:text-lg"
}
},
// 🃏 Cartes
cards: {
base: "backdrop-blur-lg rounded-2xl border transition-all duration-300",
borders: {
default: "border-banquise-blue-lightest/30",
hover: "hover:border-banquise-blue-lightest/50",
active: "border-banquise-blue/50"
},
effects: {
hover: "hover:shadow-xl",
lift: "hover:-translate-y-4 hover:shadow-2xl",
interactive: "cursor-pointer hover:-translate-y-4 hover:shadow-2xl active:scale-95"
},
backgrounds: {
glass: "bg-white/80 backdrop-blur-lg",
gradient: "bg-gradient-to-br from-white/90 to-white/80",
solid: "bg-white"
}
},
// 📝 Typographie
typography: {
headings: {
xl: "text-3xl sm:text-4xl md:text-5xl font-heading font-bold tracking-tight",
lg: "text-2xl sm:text-3xl md:text-4xl font-heading font-bold tracking-tight",
md: "text-xl sm:text-2xl md:text-3xl font-heading font-bold tracking-tight",
sm: "text-lg sm:text-xl md:text-2xl font-heading font-semibold tracking-tight"
},
body: {
xl: "text-lg sm:text-xl md:text-2xl leading-relaxed",
lg: "text-base sm:text-lg md:text-xl leading-relaxed",
md: "text-sm sm:text-base md:text-lg leading-relaxed",
sm: "text-xs sm:text-sm md:text-base leading-relaxed"
},
colors: {
primary: "text-banquise-gray",
secondary: "text-banquise-blue-dark",
muted: "text-banquise-gray/80",
light: "text-banquise-blue-lightest",
white: "text-white/90"
},
weights: {
normal: "font-normal",
medium: "font-medium",
semibold: "font-semibold",
bold: "font-bold"
}
},
// 📐 Layout
layout: {
sections: {
default: "py-12 sm:py-16 md:py-20",
compact: "py-8 sm:py-12 md:py-16",
spacious: "py-16 sm:py-20 md:py-24"
},
containers: {
default: "w-full max-w-6xl mx-auto px-4 sm:px-6 md:px-8",
narrow: "w-full max-w-4xl mx-auto px-4 sm:px-6 md:px-8",
wide: "w-full max-w-7xl mx-auto px-4 sm:px-6 md:px-8"
},
dividers: {
default: "w-20 h-1 bg-gradient-to-r from-banquise-blue-lightest to-banquise-blue mx-auto rounded-full",
small: "w-12 h-1 bg-gradient-to-r from-banquise-blue-lightest to-banquise-blue mx-auto rounded-full",
large: "w-32 h-1 bg-gradient-to-r from-banquise-blue-lightest to-banquise-blue mx-auto rounded-full"
},
grids: {
responsive: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8",
auto: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 sm:gap-6",
services: "grid grid-cols-1 lg:grid-cols-2 gap-6 lg:gap-8"
}
},
// 🎯 Icônes
icons: {
sizes: {
xs: "w-4 h-4",
sm: "w-6 h-6",
md: "w-8 h-8",
lg: "w-12 h-12",
xl: "w-16 h-16 sm:w-20 sm:h-20 lg:w-24 lg:h-24"
},
containers: {
card: "rounded-2xl flex items-center justify-center shadow-lg",
small: "w-10 h-10 rounded-lg flex items-center justify-center text-white",
service: "text-3xl sm:text-4xl lg:text-5xl"
},
effects: {
hover: "transition-transform duration-300 hover:scale-110",
spin: "animate-spin",
bounce: "animate-bounce"
}
},
// 🧭 Navigation
navigation: {
links: {
desktop: "px-4 lg:px-6 py-2.5 lg:py-3 text-white/90 hover:text-white font-medium text-sm lg:text-base rounded-xl transition-all duration-300 hover:bg-white/10 hover:backdrop-blur-sm relative group",
mobile: "group flex items-center p-4 text-white/90 hover:text-white no-underline rounded-2xl hover:bg-gradient-to-r hover:from-banquise-blue/20 hover:to-banquise-blue-light/20 transition-all duration-300 border border-transparent hover:border-banquise-blue-lightest/20"
},
effects: {
underline: "after:absolute after:bottom-1 after:left-1/2 after:w-0 after:h-0.5 after:bg-banquise-blue-lightest after:transition-all after:duration-300 group-hover:after:w-3/4 group-hover:after:left-1/8",
glow: "hover:drop-shadow-lg hover:drop-shadow-banquise-blue-lightest/50"
}
},
// ⚡ Animations & Transitions
animations: {
transitions: {
fast: "transition-all duration-200",
default: "transition-all duration-300",
slow: "transition-all duration-500"
},
transforms: {
lift: "hover:-translate-y-1",
liftLarge: "hover:-translate-y-4",
scale: "hover:scale-105",
scaleSmall: "hover:scale-102"
},
loading: {
spin: "animate-spin",
pulse: "animate-pulse",
bounce: "animate-bounce"
}
},
// 🌊 Effets spéciaux La Banquise
effects: {
ocean: {
wave: "animate-pulse",
depth: "backdrop-blur-xl",
surface: "bg-gradient-to-b from-transparent to-banquise-blue/5"
},
ice: {
crystal: "backdrop-blur-lg bg-white/10",
frost: "backdrop-blur-sm bg-white/5",
shine: "bg-gradient-to-br from-white/20 to-transparent"
}
}
} as const;
// 🎨 Utilitaires de combinaison
export const combineStyles = {
// Bouton primaire complet
primaryButton: `${designSystem.buttons.base} ${designSystem.buttons.effects} ${designSystem.gradients.primary} ${designSystem.buttons.variants.primary}`,
// Carte interactive complète
interactiveCard: `${designSystem.cards.base} ${designSystem.cards.borders.default} ${designSystem.cards.effects.interactive} ${designSystem.cards.backgrounds.glass}`,
// Section standard
standardSection: `${designSystem.layout.sections.default} ${designSystem.layout.containers.default}`,
// Titre de section
sectionTitle: `${designSystem.typography.headings.lg} ${designSystem.typography.colors.primary}`,
// Lien de navigation desktop
navLink: `${designSystem.navigation.links.desktop} ${designSystem.navigation.effects.underline}`,
// Grid responsive services
servicesGrid: designSystem.layout.grids.services
} as const;
// Type pour l'autocomplétion
export type DesignSystemKey = keyof typeof designSystem;
export type CombinedStyleKey = keyof typeof combineStyles;

View File

@ -0,0 +1,87 @@
// Utilitaires centralisés pour éviter la duplication
/**
* Combine les classes CSS en filtrant les valeurs falsy
* Remplace les fonctions mergeClasses et cn dupliquées
*/
export const cn = (...classes: (string | undefined | null | false)[]): string => {
return classes.filter(Boolean).join(' ');
};
/**
* Hook personnalisé pour gérer le scroll du body
* Factorisation de la logique répétée dans plusieurs composants
*/
export const useBodyScrollLock = (isLocked: boolean) => {
const originalStyle = document.body.style.overflow;
if (isLocked) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = originalStyle || 'unset';
}
// Cleanup function
return () => {
document.body.style.overflow = originalStyle || 'unset';
};
};
/**
* Configuration de navigation centralisée
*/
export const createNavClickHandler = (onClose?: () => void) => {
return (sectionId: string) => {
if (sectionId === 'home') {
window.scrollTo({ top: 0, behavior: 'smooth' });
} else if (sectionId === 'contact') {
window.location.href = 'mailto:contact@la-banquise.fr';
} else {
const element = document.getElementById(sectionId);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
onClose?.();
};
};
/**
* Gestionnaire d'événements de redimensionnement optimisé
*/
export const useResizeHandler = (callback: () => void, breakpoint: number = 768) => {
const handleResize = () => {
if (window.innerWidth >= breakpoint) {
callback();
}
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
};
/**
* Classes CSS communes pour éviter la répétition
*/
export const commonClasses = {
// Transitions
transition: 'transition-all duration-300 ease-in-out',
transitionFast: 'transition-all duration-200 ease-in-out',
// Hover effects communs
hoverLift: 'hover:-translate-y-1 hover:shadow-xl hover:scale-105',
hoverScale: 'hover:scale-105 active:scale-95',
// Boutons communs
buttonBase: 'inline-flex items-center justify-center font-semibold rounded-xl transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-blue-400/50',
// Navigation
navLink: 'px-4 py-2.5 text-white/90 hover:text-white font-medium rounded-xl transition-all duration-300 hover:bg-white/10',
// Cards
cardBase: 'backdrop-blur-lg rounded-2xl border transition-all duration-300',
cardHover: 'hover:shadow-xl hover:-translate-y-1',
// Mobile menu items
mobileMenuItem: 'group flex items-center justify-between p-4 rounded-xl transition-all duration-300 bg-white/5 hover:bg-white/10 border border-white/10 hover:border-white/20',
} as const;

View File

@ -1,53 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Test Couleurs Banquise</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
banquise: {
blue: '#34a6fc',
'blue-dark': '#1f5078',
'blue-light': '#76beee',
'blue-lightest': '#a0ecf9',
gray: '#F6F6F6',
},
},
}
}
}
</script>
</head>
<body class="p-8">
<h1 class="text-2xl font-bold mb-4">Test des couleurs Banquise</h1>
<div class="space-y-4">
<div class="p-4 bg-banquise-blue text-white rounded">
Couleur banquise-blue (#34a6fc)
</div>
<div class="p-4 bg-banquise-blue-dark text-white rounded">
Couleur banquise-blue-dark (#1f5078)
</div>
<div class="p-4 bg-banquise-blue-light text-white rounded">
Couleur banquise-blue-light (#76beee)
</div>
<div class="p-4 bg-banquise-blue-lightest text-black rounded">
Couleur banquise-blue-lightest (#a0ecf9)
</div>
<div class="p-4 bg-banquise-gray text-black rounded">
Couleur banquise-gray (#F6F6F6)
</div>
<button class="px-4 py-2 bg-gradient-to-r from-banquise-blue to-banquise-blue-light text-white rounded">
Bouton avec gradient banquise
</button>
</div>
</body>
</html>

View File

@ -1,9 +1,48 @@
// Re-export types from their specific modules
export type { Service } from './service';
export type { Language, Translation } from './i18n';
// Types communs pour les composants UI
export interface AccordionItemProps {
title: React.ReactNode;
children: React.ReactNode;
isOpen: boolean;
onToggle: () => void;
}
// Types pour les hooks communs
export interface UseModalReturn<T> {
data: T | null;
open: (data: T) => void;
close: () => void;
isOpen: boolean;
}
export interface UseToggleReturn {
state: boolean;
toggle: () => void;
setTrue: () => void;
setFalse: () => void;
setState: (state: boolean) => void;
}
// Types pour les composants de navigation
export interface NavigationHandler {
(sectionId: string): void;
}
// Classes CSS communes
export type CommonClassKey =
| 'transition'
| 'transitionFast'
| 'hoverLift'
| 'hoverScale'
| 'buttonBase'
| 'navLink'
| 'cardBase'
| 'cardHover'
| 'mobileMenuItem';
// Variants pour les boutons optimisés
export type ButtonVariant = 'primary' | 'discord' | 'auth' | 'secondary' | 'ghost' | 'outline';
export type ButtonSize = 'sm' | 'md' | 'lg';