WIP: update to next.js #37
@ -1,31 +1,78 @@
|
|||||||
name: Build
|
name: Build and Test
|
||||||
run-name: CI/CD website
|
run-name: Website build validation
|
||||||
|
|
||||||
on:
|
on:
|
||||||
#push:
|
push:
|
||||||
# branches:
|
branches: [main, dev]
|
||||||
# - main
|
|
||||||
# - dev
|
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize, reopened]
|
branches: [main, dev]
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- dev
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-check:
|
build-classic:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
name: Classic Build
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repository code
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Use Node.js
|
- uses: actions/setup-node@v4
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
with:
|
||||||
node-version: '24.x'
|
node-version: '20'
|
||||||
- name: Install dependencies
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 8
|
||||||
|
|
||||||
|
- name: Install and build
|
||||||
run: |
|
run: |
|
||||||
cd banquise-website
|
cd banquise-website
|
||||||
npm ci
|
pnpm install --frozen-lockfile
|
||||||
- name: Building
|
pnpm build
|
||||||
|
|
||||||
|
- name: Lint check
|
||||||
run: |
|
run: |
|
||||||
cd banquise-website
|
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
|
||||||
|
@ -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
|
|
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": ["next/core-web-vitals"]
|
|
||||||
}
|
|
243
banquise-website/.gitignore
vendored
243
banquise-website/.gitignore
vendored
@ -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
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
@ -7,18 +163,79 @@ yarn-error.log*
|
|||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
|
||||||
node_modules
|
# Diagnostic reports
|
||||||
dist
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
# Runtime data
|
||||||
.vscode/*
|
pids
|
||||||
!.vscode/extensions.json
|
*.pid
|
||||||
.idea
|
*.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
|
.DS_Store
|
||||||
*.suo
|
.DS_Store?
|
||||||
*.ntvs*
|
._*
|
||||||
*.njsproj
|
.Spotlight-V100
|
||||||
*.sln
|
.Trashes
|
||||||
*.sw?
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Local environment files
|
||||||
|
.env*.local
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
/* Minimal test CSS */
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
@ -1,13 +1,14 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
/* Configuration Tailwind v4 via CSS custom properties */
|
/* Configuration Tailwind v4 via CSS custom properties - Variables globales */
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
|
/* Polices */
|
||||||
--font-heading: 'Dela Gothic One', sans-serif;
|
--font-heading: 'Dela Gothic One', sans-serif;
|
||||||
--font-body: 'Roboto', sans-serif;
|
--font-body: 'Roboto', sans-serif;
|
||||||
|
|
||||||
/* Couleurs personnalisées La Banquise */
|
/* 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-hex: #40B4FF;
|
||||||
--color-banquise-blue-dark: 31, 93, 137;
|
--color-banquise-blue-dark: 31, 93, 137;
|
||||||
--color-banquise-blue-dark-hex: #1F5D89;
|
--color-banquise-blue-dark-hex: #1F5D89;
|
||||||
@ -16,27 +17,17 @@
|
|||||||
--color-banquise-blue-lightest: 165, 240, 255;
|
--color-banquise-blue-lightest: 165, 240, 255;
|
||||||
--color-banquise-blue-lightest-hex: #A5F0FF;
|
--color-banquise-blue-lightest-hex: #A5F0FF;
|
||||||
--color-banquise-gray: #F6F6F6;
|
--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 */
|
/* Minimal, valid utility helpers avec variables optimisé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() */
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
/* Text colors */
|
/* Text colors */
|
||||||
.text-banquise-blue { color: var(--color-banquise-blue-hex); }
|
.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-blue-lightest { background-color: var(--color-banquise-blue-lightest-hex); }
|
||||||
.bg-banquise-gray { background-color: var(--color-banquise-gray); }
|
.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-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-10 { background-color: rgba(var(--color-banquise-blue), 0.10); }
|
||||||
.bg-banquise-blue-20 { background-color: rgba(var(--color-banquise-blue), 0.20); }
|
.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 { border-color: var(--color-banquise-blue-hex); }
|
||||||
.border-banquise-blue-lightest-30 { border-color: rgba(var(--color-banquise-blue-lightest), 0.3); }
|
.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 { --tw-gradient-from: var(--color-banquise-blue-hex); }
|
||||||
.from-banquise-blue-dark { --tw-gradient-from: var(--color-banquise-blue-dark-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); }
|
.via-banquise-blue { --tw-gradient-via: var(--color-banquise-blue-hex); }
|
||||||
.to-banquise-blue { --tw-gradient-to: 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); }
|
.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 {
|
@keyframes gentle-float {
|
||||||
0%, 100% { transform: translateY(0) rotate(0deg); }
|
0%, 100% { transform: translateY(0) rotate(0deg); }
|
||||||
50% { transform: translateY(-15px) rotate(1deg); }
|
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; }
|
.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) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.animate-gentle-float,
|
.animate-gentle-float,
|
||||||
.animate-ping,
|
.animate-ping,
|
||||||
@ -92,13 +90,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Global improvements */
|
|
||||||
html { scroll-behavior: smooth; }
|
html { scroll-behavior: smooth; }
|
||||||
body { overflow-x: hidden; }
|
body { overflow-x: hidden; }
|
||||||
|
|
||||||
/* Scrollbar styles for popup content */
|
/* Scrollbar unifié pour tous les éléments */
|
||||||
.popup-content { scrollbar-width: thin; scrollbar-color: rgba(31,93,137,0.3) transparent; }
|
.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; }
|
.popup-content::-webkit-scrollbar { width: 6px; }
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track,
|
||||||
.popup-content::-webkit-scrollbar-track { background: transparent; }
|
.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);
|
||||||
|
}
|
||||||
|
@ -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'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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { cn, commonClasses } from '@/lib/utils';
|
||||||
|
|
||||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
variant?: 'primary' | 'discord' | 'auth' | 'secondary' | 'ghost' | 'outline';
|
variant?: 'primary' | 'discord' | 'auth' | 'secondary' | 'ghost' | 'outline';
|
||||||
@ -10,11 +11,12 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Factorisation des classes de taille et de variante
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
sm: 'px-4 py-2 text-sm',
|
sm: 'px-4 py-2 text-sm',
|
||||||
md: 'px-6 py-3 text-base',
|
md: 'px-6 py-3 text-base',
|
||||||
lg: 'px-8 py-4 text-lg',
|
lg: 'px-8 py-4 text-lg',
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
const variantClasses = {
|
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',
|
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',
|
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',
|
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',
|
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> = ({
|
export const Button: React.FC<ButtonProps> = ({
|
||||||
variant = 'primary',
|
variant = 'primary',
|
||||||
@ -37,28 +47,25 @@ export const Button: React.FC<ButtonProps> = ({
|
|||||||
disabled,
|
disabled,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const baseClasses = [
|
const isDisabled = disabled || loading;
|
||||||
'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(' ');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={baseClasses}
|
className={cn(
|
||||||
disabled={disabled || loading}
|
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}
|
{...props}
|
||||||
>
|
>
|
||||||
{loading && (
|
{loading && <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>
|
|
||||||
)}
|
|
||||||
{leftIcon && !loading && <span className="mr-2">{leftIcon}</span>}
|
{leftIcon && !loading && <span className="mr-2">{leftIcon}</span>}
|
||||||
{children}
|
{children}
|
||||||
{rightIcon && !loading && <span className="ml-2">{rightIcon}</span>}
|
{rightIcon && !loading && <span className="ml-2">{rightIcon}</span>}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
import { cn, commonClasses } from '@/lib/utils';
|
||||||
import type { Service } from '@/types/service';
|
import type { Service } from '@/types/service';
|
||||||
|
|
||||||
interface ServiceCardProps {
|
interface ServiceCardProps {
|
||||||
@ -8,6 +9,17 @@ interface ServiceCardProps {
|
|||||||
className?: string;
|
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> = ({
|
export const ServiceCard: React.FC<ServiceCardProps> = ({
|
||||||
service,
|
service,
|
||||||
onServiceClick,
|
onServiceClick,
|
||||||
@ -19,7 +31,15 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
{/* Indicateur de survol */}
|
{/* Indicateur de survol */}
|
||||||
@ -28,34 +48,45 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({
|
|||||||
{/* Contenu de la carte */}
|
{/* Contenu de la carte */}
|
||||||
<div className="relative z-10">
|
<div className="relative z-10">
|
||||||
{/* Icône du service */}
|
{/* 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
|
<Image
|
||||||
src={service.image as any}
|
src={service.image as any}
|
||||||
alt={service.icon}
|
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}
|
width={48}
|
||||||
height={48}
|
height={48}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Nom du service */}
|
{/* 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}
|
{service.name}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{/* Description courte */}
|
{/* 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]}.
|
{service.description.split('.')[0]}.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Flèche indicatrice au hover */}
|
{/* 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">
|
<HoverArrow />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -2,91 +2,120 @@ import React from 'react';
|
|||||||
import { URLS, SITE_CONFIG } from '@/lib/config/constants';
|
import { URLS, SITE_CONFIG } from '@/lib/config/constants';
|
||||||
import { BookOpen, GitBranch, Gamepad2, Cloud, Rocket, Heart } from 'lucide-react';
|
import { BookOpen, GitBranch, Gamepad2, Cloud, Rocket, Heart } from 'lucide-react';
|
||||||
import { useTranslation } from '@/lib/hooks/useTranslation';
|
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 = () => {
|
export const Footer: React.FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="bg-gray-900 text-white py-16 px-4 sm:px-6 lg:px-8 border-t border-gray-800">
|
<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">
|
<div className="max-w-7xl mx-auto">
|
||||||
{/* Contenu principal du footer */}
|
{/* Contenu principal du footer */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-12 mb-12">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-12 mb-12">
|
||||||
|
|
||||||
{/* Marque et description */}
|
{/* Marque et description */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center gap-4">
|
<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">
|
<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>
|
<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>
|
</div>
|
||||||
<span className="text-white font-bold text-2xl">
|
|
||||||
{SITE_CONFIG.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
</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 */}
|
{/* Liens rapides services */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h3 className="text-white font-bold text-lg mb-6">{t.footer.ourServices}</h3>
|
<h3 className="text-white font-bold text-lg mb-6">{t.footer.ourServices}</h3>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<a
|
<ServiceLink href={URLS.services.wiki} icon={BookOpen}>
|
||||||
href={URLS.services.wiki}
|
Wiki
|
||||||
className="flex items-center text-gray-300 hover:text-blue-400 transition-colors duration-200 hover:translate-x-2 transform"
|
</ServiceLink>
|
||||||
>
|
<ServiceLink href={URLS.services.gitea} icon={GitBranch}>
|
||||||
<BookOpen className="w-5 h-5 mr-3" strokeWidth={2} />
|
Gitea
|
||||||
Wiki
|
</ServiceLink>
|
||||||
</a>
|
<ServiceLink href={URLS.services.panel} icon={Gamepad2}>
|
||||||
<a
|
{t.footer.gamingPanel}
|
||||||
href={URLS.services.gitea}
|
</ServiceLink>
|
||||||
className="flex items-center text-gray-300 hover:text-blue-400 transition-colors duration-200 hover:translate-x-2 transform"
|
<ServiceLink href={URLS.services.opencloud} icon={Cloud}>
|
||||||
>
|
OpenCloud
|
||||||
<GitBranch className="w-5 h-5 mr-3" strokeWidth={2} />
|
</ServiceLink>
|
||||||
Gitea
|
</div>
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Informations communauté */}
|
{/* Informations communauté */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h3 className="text-white font-bold text-lg mb-6">{t.footer.community}</h3>
|
<h3 className="text-white font-bold text-lg mb-6">{t.footer.community}</h3>
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="bg-gradient-to-r from-blue-900/30 to-blue-800/30 rounded-xl p-6 border border-blue-800/30">
|
<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>
|
<h4 className="text-blue-400 font-semibold mb-2">{t.footer.joinAssociation}</h4>
|
||||||
<p className="text-gray-300 text-sm mb-4">
|
<p className="text-gray-300 text-sm mb-4">
|
||||||
@ -94,7 +123,11 @@ export const Footer: React.FC = () => {
|
|||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
href={URLS.social.discord}
|
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} />
|
<Rocket className="w-4 h-4 mr-2" strokeWidth={2} />
|
||||||
{t.footer.joinNow}
|
{t.footer.joinNow}
|
||||||
@ -102,24 +135,23 @@ export const Footer: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Barre du bas */}
|
{/* Barre du bas */}
|
||||||
<div className="flex flex-col md:flex-row justify-between items-center gap-6 pt-8 border-t border-gray-800">
|
<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">
|
<p className="text-gray-400 text-sm text-center md:text-left">
|
||||||
© 2025 {SITE_CONFIG.name}. {t.footer.copyright}
|
© 2025 {SITE_CONFIG.name}. {t.footer.copyright}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-6 text-sm text-gray-400">
|
<div className="flex items-center gap-6 text-sm text-gray-400">
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
{t.footer.madeWith}
|
{t.footer.madeWith}
|
||||||
<Heart className="text-red-500 mx-1 w-4 h-4" strokeWidth={2} fill="currentColor" />
|
<Heart className="text-red-500 mx-1 w-4 h-4" strokeWidth={2} fill="currentColor" />
|
||||||
{t.footer.by}
|
{t.footer.by}
|
||||||
</span>
|
</span>
|
||||||
<div className="w-1 h-1 bg-gray-600 rounded-full"></div>
|
<div className="w-1 h-1 bg-gray-600 rounded-full"></div>
|
||||||
<span className="text-blue-400 font-semibold">EPITA 2025</span>
|
<span className="text-blue-400 font-semibold">EPITA 2025</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</footer>
|
||||||
</footer>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -3,13 +3,9 @@ import { Button } from '@/components/common/Button';
|
|||||||
import { Logo } from './navbar/Logo';
|
import { Logo } from './navbar/Logo';
|
||||||
import { URLS } from '@/lib/config/constants';
|
import { URLS } from '@/lib/config/constants';
|
||||||
import { useTranslation } from '@/lib/hooks/useTranslation';
|
import { useTranslation } from '@/lib/hooks/useTranslation';
|
||||||
|
import { cn, createNavClickHandler, commonClasses } from '@/lib/utils';
|
||||||
import type { Translation } from '@/types/i18n';
|
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 {
|
interface MobileMenuProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@ -45,10 +41,8 @@ const MobileNavItem: React.FC<MobileNavItemProps> = ({
|
|||||||
href={href}
|
href={href}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
'group flex items-center justify-between p-4 rounded-xl transition-all duration-300',
|
commonClasses.mobileMenuItem,
|
||||||
'bg-white/5 hover:bg-white/10 active:bg-white/15',
|
commonClasses.hoverScale,
|
||||||
'border border-white/10 hover:border-white/20',
|
|
||||||
'hover:scale-[1.02] active:scale-[0.98]',
|
|
||||||
'hover:shadow-lg hover:shadow-blue-500/20'
|
'hover:shadow-lg hover:shadow-blue-500/20'
|
||||||
)}
|
)}
|
||||||
target={isExternal ? '_blank' : undefined}
|
target={isExternal ? '_blank' : undefined}
|
||||||
@ -89,40 +83,57 @@ const MobileNavItem: React.FC<MobileNavItemProps> = ({
|
|||||||
|
|
||||||
export const MobileMenu: React.FC<MobileMenuProps> = ({ isOpen, onClose, translations }) => {
|
export const MobileMenu: React.FC<MobileMenuProps> = ({ isOpen, onClose, translations }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
// Gérer le scroll du body
|
|
||||||
|
// Gérer le scroll du body - simplifié avec notre utilitaire
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const originalStyle = document.body.style.overflow;
|
||||||
|
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
} else {
|
} else {
|
||||||
document.body.style.overflow = 'unset';
|
document.body.style.overflow = originalStyle || 'unset';
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.body.style.overflow = 'unset';
|
document.body.style.overflow = originalStyle || 'unset';
|
||||||
};
|
};
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
const handleNavClick = (sectionId: string) => {
|
// Gestionnaire de navigation optimisé
|
||||||
if (sectionId === 'home') {
|
const handleNavClick = createNavClickHandler(onClose);
|
||||||
// Scroll to top for home section
|
|
||||||
window.scrollTo({
|
// Configuration des icônes SVG - factorisation
|
||||||
top: 0,
|
const icons = {
|
||||||
behavior: 'smooth'
|
home: (
|
||||||
});
|
<svg className="w-5 h-5 text-blue-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
} else if (sectionId === 'contact') {
|
<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" />
|
||||||
// Open email client for contact
|
</svg>
|
||||||
window.location.href = 'mailto:contact@la-banquise.fr';
|
),
|
||||||
} else {
|
services: (
|
||||||
// Scroll to specific section
|
<svg className="w-5 h-5 text-blue-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
const element = document.getElementById(sectionId);
|
<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" />
|
||||||
if (element) {
|
</svg>
|
||||||
element.scrollIntoView({
|
),
|
||||||
behavior: 'smooth',
|
about: (
|
||||||
block: 'start'
|
<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>
|
||||||
}
|
),
|
||||||
onClose();
|
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 (
|
return (
|
||||||
@ -156,10 +167,11 @@ export const MobileMenu: React.FC<MobileMenuProps> = ({ isOpen, onClose, transla
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
className={cn(
|
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',
|
'bg-white/10 hover:bg-white/20 active:bg-white/25',
|
||||||
'border border-white/20 hover:border-white/30',
|
'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'
|
'focus:outline-none focus:ring-2 focus:ring-blue-400/50'
|
||||||
)}
|
)}
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
@ -177,11 +189,7 @@ export const MobileMenu: React.FC<MobileMenuProps> = ({ isOpen, onClose, transla
|
|||||||
{/* Section Navigation */}
|
{/* Section Navigation */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<MobileNavItem
|
<MobileNavItem
|
||||||
icon={
|
icon={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>
|
|
||||||
}
|
|
||||||
title={translations.home}
|
title={translations.home}
|
||||||
description={t.common.backToHome}
|
description={t.common.backToHome}
|
||||||
href="#home"
|
href="#home"
|
||||||
@ -189,11 +197,7 @@ export const MobileMenu: React.FC<MobileMenuProps> = ({ isOpen, onClose, transla
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<MobileNavItem
|
<MobileNavItem
|
||||||
icon={
|
icon={icons.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>
|
|
||||||
}
|
|
||||||
title={translations.services}
|
title={translations.services}
|
||||||
description={t.common.discoverOffer}
|
description={t.common.discoverOffer}
|
||||||
href="#services"
|
href="#services"
|
||||||
@ -201,11 +205,7 @@ export const MobileMenu: React.FC<MobileMenuProps> = ({ isOpen, onClose, transla
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<MobileNavItem
|
<MobileNavItem
|
||||||
icon={
|
icon={icons.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>
|
|
||||||
}
|
|
||||||
title={translations.about}
|
title={translations.about}
|
||||||
description={t.common.learnMoreAboutUs}
|
description={t.common.learnMoreAboutUs}
|
||||||
href="#about"
|
href="#about"
|
||||||
@ -213,11 +213,7 @@ export const MobileMenu: React.FC<MobileMenuProps> = ({ isOpen, onClose, transla
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<MobileNavItem
|
<MobileNavItem
|
||||||
icon={
|
icon={icons.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>
|
|
||||||
}
|
|
||||||
title={translations.contact}
|
title={translations.contact}
|
||||||
description={t.common.sendEmail}
|
description={t.common.sendEmail}
|
||||||
href="mailto:contact@la-banquise.fr"
|
href="mailto:contact@la-banquise.fr"
|
||||||
@ -231,11 +227,7 @@ export const MobileMenu: React.FC<MobileMenuProps> = ({ isOpen, onClose, transla
|
|||||||
{/* Social & External Links */}
|
{/* Social & External Links */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<MobileNavItem
|
<MobileNavItem
|
||||||
icon={
|
icon={icons.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>
|
|
||||||
}
|
|
||||||
title="Discord"
|
title="Discord"
|
||||||
description={t.common.joinCommunity}
|
description={t.common.joinCommunity}
|
||||||
href={URLS.social.discord}
|
href={URLS.social.discord}
|
||||||
@ -248,11 +240,7 @@ export const MobileMenu: React.FC<MobileMenuProps> = ({ isOpen, onClose, transla
|
|||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="lg"
|
size="lg"
|
||||||
leftIcon={
|
leftIcon={icons.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>
|
|
||||||
}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.open(URLS.services.auth, '_blank');
|
window.open(URLS.services.auth, '_blank');
|
||||||
onClose();
|
onClose();
|
||||||
|
@ -5,13 +5,9 @@ import { NavLinks } from './navbar/NavLinks';
|
|||||||
import { ActionButtons } from './navbar/ActionButtons';
|
import { ActionButtons } from './navbar/ActionButtons';
|
||||||
import { MobileMenuButton } from './navbar/MobileMenuButton';
|
import { MobileMenuButton } from './navbar/MobileMenuButton';
|
||||||
import { MobileMenu } from './MobileMenu';
|
import { MobileMenu } from './MobileMenu';
|
||||||
|
import { cn, useResizeHandler } from '@/lib/utils';
|
||||||
import type { Translation } from '@/types/i18n';
|
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 {
|
interface ModernNavigationProps {
|
||||||
translations: Translation['navigation'];
|
translations: Translation['navigation'];
|
||||||
languageSwitcher: React.ReactElement;
|
languageSwitcher: React.ReactElement;
|
||||||
@ -24,68 +20,35 @@ export const ModernNavigation: React.FC<ModernNavigationProps> = ({
|
|||||||
const { scrolled } = useScrollEffects();
|
const { scrolled } = useScrollEffects();
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);
|
const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);
|
||||||
|
|
||||||
// Fermer le menu mobile lors du redimensionnement
|
// Fermer le menu mobile lors du redimensionnement - optimisé
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const handleResize = () => {
|
const cleanup = useResizeHandler(() => setMobileMenuOpen(false));
|
||||||
if (window.innerWidth >= 768) {
|
return cleanup;
|
||||||
setMobileMenuOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize);
|
|
||||||
return () => window.removeEventListener('resize', handleResize);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Navigation moderne épurée */}
|
{/* Navigation moderne épurée */}
|
||||||
<nav className={mergeClasses(
|
<nav className={cn(
|
||||||
// Position et z-index
|
|
||||||
'fixed top-0 left-0 right-0 z-50',
|
'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',
|
'bg-blue-700/95 backdrop-blur-md border-b border-blue-600/30',
|
||||||
|
'transition-fast',
|
||||||
// Transition fluide
|
|
||||||
'transition-all duration-200 ease-in-out',
|
|
||||||
|
|
||||||
// Effet de scroll
|
|
||||||
scrolled && 'shadow-lg'
|
scrolled && 'shadow-lg'
|
||||||
)}>
|
)}>
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
<div className="flex justify-between items-center px-4 sm:px-6 lg:px-8 h-16">
|
<div className="flex justify-between items-center px-4 sm:px-6 lg:px-8 h-16">
|
||||||
|
|
||||||
{/* Logo Section */}
|
|
||||||
<Logo scrolled={scrolled} />
|
<Logo scrolled={scrolled} />
|
||||||
|
|
||||||
{/* Navigation Links (Desktop) - Centré */}
|
|
||||||
<div className="flex-1 flex justify-center">
|
<div className="flex-1 flex justify-center">
|
||||||
<NavLinks
|
<NavLinks translations={translations} scrolled={scrolled} />
|
||||||
translations={translations}
|
|
||||||
scrolled={scrolled}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Buttons (Desktop) */}
|
|
||||||
<ActionButtons
|
<ActionButtons
|
||||||
scrolled={scrolled}
|
scrolled={scrolled}
|
||||||
languageSwitcher={languageSwitcher}
|
languageSwitcher={languageSwitcher}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Mobile Menu Button */}
|
|
||||||
<MobileMenuButton
|
<MobileMenuButton
|
||||||
isOpen={mobileMenuOpen}
|
isOpen={mobileMenuOpen}
|
||||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||||
@ -100,7 +63,6 @@ export const ModernNavigation: React.FC<ModernNavigationProps> = ({
|
|||||||
{/* Spacer pour compenser la navbar fixed */}
|
{/* Spacer pour compenser la navbar fixed */}
|
||||||
<div className="h-16" />
|
<div className="h-16" />
|
||||||
|
|
||||||
{/* Menu Mobile */}
|
|
||||||
<MobileMenu
|
<MobileMenu
|
||||||
isOpen={mobileMenuOpen}
|
isOpen={mobileMenuOpen}
|
||||||
onClose={() => setMobileMenuOpen(false)}
|
onClose={() => setMobileMenuOpen(false)}
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { AccordionItem } from '@/components/ui/AccordionItem';
|
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 { URLS } from '@/lib/config/constants';
|
||||||
import { Target, Settings, HelpCircle, Users, MessageCircle, Rocket, BookOpen, GitBranch, Gamepad2, Bird, Building, Mail, Cloud } from 'lucide-react';
|
import { Target, Settings, HelpCircle, Users, MessageCircle, Rocket, BookOpen, GitBranch, Gamepad2, Bird, Building, Mail, Cloud } from 'lucide-react';
|
||||||
import { useTranslation } from '@/lib/hooks/useTranslation';
|
import { useTranslation } from '@/lib/hooks/useTranslation';
|
||||||
|
import { cn, commonClasses } from '@/lib/utils';
|
||||||
|
|
||||||
interface AboutSectionProps {
|
interface AboutSectionProps {
|
||||||
openAccordion: string | null;
|
openAccordion: string | null;
|
||||||
@ -15,18 +19,11 @@ export const AboutSection: React.FC<AboutSectionProps> = ({ openAccordion, toggl
|
|||||||
return (
|
return (
|
||||||
<section id="about" className="py-24 md:py-32 px-4 sm:px-6 lg:px-8 bg-white">
|
<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">
|
<div className="max-w-6xl mx-auto">
|
||||||
{/* Header de section moderne */}
|
{/* Header de section moderne - factorisation */}
|
||||||
<div className="text-center mb-20">
|
<SectionHeader
|
||||||
{/* Séparateur visuel */}
|
title={t.about.title}
|
||||||
<div className="w-24 h-1.5 bg-gradient-to-r from-blue-600 to-blue-400 rounded-full mx-auto mb-8" />
|
subtitle={t.about.subtitle}
|
||||||
|
/>
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Section FAQ avec design moderne */}
|
{/* Section FAQ avec design moderne */}
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
@ -38,14 +35,7 @@ export const AboutSection: React.FC<AboutSectionProps> = ({ openAccordion, toggl
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
title={
|
title={<AccordionTitle icon={Target} title={t.about.mission.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>
|
|
||||||
}
|
|
||||||
isOpen={openAccordion === "mission"}
|
isOpen={openAccordion === "mission"}
|
||||||
onToggle={() => toggleAccordion("mission")}
|
onToggle={() => toggleAccordion("mission")}
|
||||||
>
|
>
|
||||||
@ -64,151 +54,118 @@ export const AboutSection: React.FC<AboutSectionProps> = ({ openAccordion, toggl
|
|||||||
</div>
|
</div>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
title={
|
title={<AccordionTitle icon={Settings} title={t.about.services.title} />}
|
||||||
<div className="flex items-center">
|
isOpen={openAccordion === "services"}
|
||||||
<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">
|
onToggle={() => toggleAccordion("services")}
|
||||||
<Settings className="w-5 h-5" strokeWidth={2} />
|
>
|
||||||
|
<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>
|
</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>
|
</div>
|
||||||
}
|
</AccordionItem>
|
||||||
isOpen={openAccordion === "services"}
|
|
||||||
onToggle={() => toggleAccordion("services")}
|
<AccordionItem
|
||||||
>
|
title={<AccordionTitle icon={Users} title={t.about.community.title} />}
|
||||||
<div className="space-y-6 p-6 bg-gray-50 rounded-xl">
|
isOpen={openAccordion === "community"}
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
onToggle={() => toggleAccordion("community")}
|
||||||
{/* 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="space-y-8 p-6 bg-gray-50 rounded-xl">
|
||||||
<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">
|
<p className="text-gray-700 text-lg leading-relaxed">
|
||||||
<BookOpen className="w-6 h-6" strokeWidth={2} />
|
{t.about.community.description}
|
||||||
</div>
|
</p>
|
||||||
<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>
|
|
||||||
|
|
||||||
<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="bg-gradient-to-r from-blue-50 to-indigo-50 border-2 border-blue-200 rounded-2xl p-8">
|
||||||
<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">
|
<h4 className="font-bold text-gray-900 mb-6 flex items-center text-xl">
|
||||||
<GitBranch className="w-6 h-6" strokeWidth={2} />
|
<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">
|
||||||
</div>
|
<MessageCircle className="w-5 h-5" strokeWidth={2} />
|
||||||
<div>
|
</div>
|
||||||
<h4 className="font-bold text-gray-900 mb-2 text-lg">{t.about.services.gitea.title}</h4>
|
{t.about.community.howToJoin}
|
||||||
<p className="text-gray-600">{t.about.services.gitea.description}</p>
|
</h4>
|
||||||
</div>
|
<ul className="space-y-4 text-gray-700 mb-8">
|
||||||
</div>
|
<li className="flex items-center text-lg">
|
||||||
|
<span className="w-2 h-2 bg-blue-500 rounded-full mr-4"></span>
|
||||||
<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">
|
{t.about.community.steps.step1}
|
||||||
<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">
|
</li>
|
||||||
<Gamepad2 className="w-6 h-6" strokeWidth={2} />
|
<li className="flex items-center text-lg">
|
||||||
</div>
|
<span className="w-2 h-2 bg-blue-500 rounded-full mr-4"></span>
|
||||||
<div>
|
{t.about.community.steps.step2}
|
||||||
<h4 className="font-bold text-gray-900 mb-2 text-lg">{t.about.services.panel.title}</h4>
|
</li>
|
||||||
<p className="text-gray-600">{t.about.services.panel.description}</p>
|
<li className="flex items-center text-lg">
|
||||||
</div>
|
<span className="w-2 h-2 bg-blue-500 rounded-full mr-4"></span>
|
||||||
</div>
|
{t.about.community.steps.step3}
|
||||||
|
</li>
|
||||||
<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">
|
</ul>
|
||||||
<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} />
|
<a
|
||||||
</div>
|
href={URLS.social.discord}
|
||||||
<div>
|
className={cn(
|
||||||
<h4 className="font-bold text-gray-900 mb-2 text-lg">{t.about.services.pelican.title}</h4>
|
'inline-flex items-center justify-center',
|
||||||
<p className="text-gray-600">{t.about.services.pelican.description}</p>
|
'px-8 py-4 text-lg font-bold text-white',
|
||||||
</div>
|
'bg-gradient-to-r from-blue-600 to-blue-500 rounded-xl',
|
||||||
</div>
|
'shadow-xl hover:shadow-2xl hover:from-blue-700 hover:to-blue-600',
|
||||||
|
commonClasses.transition,
|
||||||
<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">
|
commonClasses.hoverScale,
|
||||||
<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">
|
'border-2 border-blue-600/20'
|
||||||
<Building className="w-6 h-6" strokeWidth={2} />
|
)}
|
||||||
</div>
|
>
|
||||||
<div>
|
<Rocket className="w-6 h-6 mr-3" strokeWidth={2} />
|
||||||
<h4 className="font-bold text-gray-900 mb-2 text-lg">{t.about.services.intranet.title}</h4>
|
{t.about.community.joinDiscord}
|
||||||
<p className="text-gray-600">{t.about.services.intranet.description}</p>
|
</a>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-600 mt-6 p-4 bg-blue-50 rounded-lg border border-blue-200">
|
</AccordionItem>
|
||||||
<strong className="text-blue-800 flex items-center">
|
</div>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { ArrowRight } from 'lucide-react';
|
import { ArrowRight } from 'lucide-react';
|
||||||
|
import { cn, commonClasses } from '@/lib/utils';
|
||||||
import type { Translation } from '@/types/i18n';
|
import type { Translation } from '@/types/i18n';
|
||||||
|
|
||||||
interface HeroSectionProps {
|
interface HeroSectionProps {
|
||||||
@ -8,12 +9,69 @@ interface HeroSectionProps {
|
|||||||
commonTranslations: Translation['common'];
|
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 }) => (
|
export const HeroSection: React.FC<HeroSectionProps> = ({ translations, commonTranslations }) => (
|
||||||
<section
|
<section
|
||||||
id="home"
|
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"
|
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
|
<div
|
||||||
className="absolute inset-0 opacity-40"
|
className="absolute inset-0 opacity-40"
|
||||||
style={{
|
style={{
|
||||||
@ -21,8 +79,6 @@ export const HeroSection: React.FC<HeroSectionProps> = ({ translations, commonTr
|
|||||||
backgroundSize: '32px 32px'
|
backgroundSize: '32px 32px'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Grille secondaire pour plus de profondeur */}
|
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 opacity-20"
|
className="absolute inset-0 opacity-20"
|
||||||
style={{
|
style={{
|
||||||
@ -37,17 +93,24 @@ export const HeroSection: React.FC<HeroSectionProps> = ({ translations, commonTr
|
|||||||
{/* Logo principal avec effet moderne */}
|
{/* Logo principal avec effet moderne */}
|
||||||
<div className="mb-16 group">
|
<div className="mb-16 group">
|
||||||
<div className="relative inline-block">
|
<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" />
|
<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={cn(
|
||||||
<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">
|
'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
|
<Image
|
||||||
src="/assets/banquise_server.svg"
|
src="/assets/banquise_server.svg"
|
||||||
alt={translations.title}
|
alt={translations.title}
|
||||||
width={140}
|
width={140}
|
||||||
height={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={{
|
style={{
|
||||||
filter: 'drop-shadow(0 8px 24px rgba(59, 130, 246, 0.4))'
|
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}
|
{translations.subtitle}
|
||||||
</p>
|
</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">
|
<div className="flex flex-col sm:flex-row gap-6 justify-center items-center mb-20">
|
||||||
{/* Bouton principal très visible */}
|
<CTAButton
|
||||||
<a
|
|
||||||
href="#services"
|
href="#services"
|
||||||
onClick={(e) => {
|
primary
|
||||||
e.preventDefault();
|
icon={<ArrowRight className="w-6 h-6 transition-transform duration-300 group-hover:translate-x-2" strokeWidth={2.5} />}
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
{/* Effet de brillance au hover */}
|
{translations.cta}
|
||||||
<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" />
|
</CTAButton>
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Bouton secondaire épuré */}
|
<CTAButton href="#about">
|
||||||
<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"
|
|
||||||
>
|
|
||||||
{commonTranslations.learnMore}
|
{commonTranslations.learnMore}
|
||||||
</a>
|
</CTAButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ServiceCard } from '@/components/common/ServiceCard';
|
import { ServiceCard } from '@/components/common/ServiceCard';
|
||||||
|
import { SectionHeader } from '@/components/ui/SectionHeader';
|
||||||
import { useTranslation } from '@/lib/hooks/useTranslation';
|
import { useTranslation } from '@/lib/hooks/useTranslation';
|
||||||
import type { Service } from '@/types/service';
|
import type { Service } from '@/types/service';
|
||||||
|
|
||||||
@ -23,34 +24,24 @@ export const ServicesSection: React.FC<ServicesSectionProps> = ({
|
|||||||
id="services"
|
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"
|
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">
|
<div className="max-w-7xl mx-auto">
|
||||||
{/* Header de section moderne avec forte hiérarchie */}
|
{/* Header de section moderne avec forte hiérarchie - factorisation */}
|
||||||
<div className="text-center mb-20">
|
<SectionHeader
|
||||||
{/* Séparateur visuel moderne */}
|
title={t.sections.ourServices}
|
||||||
<div className="w-24 h-1.5 bg-gradient-to-r from-blue-600 to-blue-400 rounded-full mx-auto mb-8" />
|
subtitle={translations.discoverFeatures}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Titre principal avec contraste fort */}
|
{/* Grille de services avec espacement généreux */}
|
||||||
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-gray-900 mb-6 leading-tight">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-12">
|
||||||
{t.sections.ourServices}
|
{services.map((service) => (
|
||||||
</h2>
|
<ServiceCard
|
||||||
|
key={service.name}
|
||||||
{/* Sous-titre avec bon contraste */}
|
service={service}
|
||||||
<p className="text-lg md:text-xl text-gray-700 mx-auto max-w-3xl leading-relaxed font-medium">
|
onServiceClick={onServiceClick}
|
||||||
{translations.discoverFeatures}
|
/>
|
||||||
</p>
|
))}
|
||||||
|
</div>
|
||||||
</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>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
17
banquise-website/components/ui/AccordionTitle.tsx
Normal file
17
banquise-website/components/ui/AccordionTitle.tsx
Normal 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>
|
||||||
|
);
|
@ -1,5 +1,7 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { URLS } from '@/lib/config/constants';
|
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 { Service } from '@/types/service';
|
||||||
import type { Translation } from '@/types/i18n';
|
import type { Translation } from '@/types/i18n';
|
||||||
import { ClipboardList, Zap, Check, Lock, Rocket } from 'lucide-react';
|
import { ClipboardList, Zap, Check, Lock, Rocket } from 'lucide-react';
|
||||||
@ -13,10 +15,11 @@ interface PopupProps {
|
|||||||
export const Popup: React.FC<PopupProps> = ({ service, onClose, translations }) => {
|
export const Popup: React.FC<PopupProps> = ({ service, onClose, translations }) => {
|
||||||
// Empêcher le scroll du body quand la popup est ouverte
|
// Empêcher le scroll du body quand la popup est ouverte
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const originalStyle = document.body.style.overflow;
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
|
|
||||||
return () => {
|
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">
|
<div className="absolute top-4 right-4 z-50">
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
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}
|
aria-label={translations.close}
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
@ -62,53 +72,30 @@ export const Popup: React.FC<PopupProps> = ({ service, onClose, translations })
|
|||||||
{/* Content - Forcer le fond blanc */}
|
{/* Content - Forcer le fond blanc */}
|
||||||
<div className="p-6 sm:p-8 bg-white">
|
<div className="p-6 sm:p-8 bg-white">
|
||||||
{/* Description */}
|
{/* 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">
|
<SectionTitle icon={ClipboardList} title="Description détaillée" />
|
||||||
<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>
|
|
||||||
<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">
|
<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">
|
<p className="text-gray-700 leading-relaxed text-base sm:text-lg lg:text-xl mb-4">
|
||||||
{service.description}
|
{service.description}
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-6">
|
<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">
|
<FeatureBadge
|
||||||
<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={Check}
|
||||||
<Check className="w-5 h-5" strokeWidth={2} />
|
title="99.9% Uptime"
|
||||||
</div>
|
subtitle="Disponibilité garantie"
|
||||||
<div>
|
/>
|
||||||
<div className="font-semibold text-gray-800 text-sm">99.9% Uptime</div>
|
<FeatureBadge
|
||||||
<div className="text-gray-600 text-xs">Disponibilité garantie</div>
|
icon={Lock}
|
||||||
</div>
|
title="Sécurisé"
|
||||||
</div>
|
subtitle="SSL & Backups"
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Fonctionnalités */}
|
{/* 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">
|
<SectionTitle icon={Zap} title={translations.discoverFeatures} />
|
||||||
<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>
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
|
||||||
{service.features.map((feature, index) => (
|
{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">
|
<FeatureItem key={index} feature={feature} index={index} />
|
||||||
<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>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -118,14 +105,23 @@ export const Popup: React.FC<PopupProps> = ({ service, onClose, translations })
|
|||||||
href={service.url}
|
href={service.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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} />
|
<Rocket className="w-6 h-6 lg:w-7 lg:h-7 mr-3" strokeWidth={2} />
|
||||||
<span>Accéder à {service.name}</span>
|
<span>Accéder à {service.name}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<p className="text-center text-sm text-gray-500 mt-4">
|
<p className="text-center text-sm text-gray-500 mt-4">
|
||||||
Besoin d'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'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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
53
banquise-website/components/ui/PopupComponents.tsx
Normal file
53
banquise-website/components/ui/PopupComponents.tsx
Normal 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>
|
||||||
|
);
|
31
banquise-website/components/ui/SectionHeader.tsx
Normal file
31
banquise-website/components/ui/SectionHeader.tsx
Normal 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>
|
||||||
|
);
|
33
banquise-website/components/ui/ServiceCardAbout.tsx
Normal file
33
banquise-website/components/ui/ServiceCardAbout.tsx
Normal 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>
|
||||||
|
);
|
@ -2,7 +2,7 @@ import { useState, useCallback } from 'react';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook personnalisé pour gérer l'état des accordéons
|
* 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) => {
|
export const useAccordion = (initialState: string | null = null) => {
|
||||||
const [openAccordion, setOpenAccordion] = useState<string | null>(initialState);
|
const [openAccordion, setOpenAccordion] = useState<string | null>(initialState);
|
||||||
|
118
banquise-website/lib/hooks/useCommon.ts
Normal file
118
banquise-website/lib/hooks/useCommon.ts
Normal 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
|
||||||
|
};
|
||||||
|
};
|
@ -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;
|
|
197
banquise-website/lib/styles/designSystem.ts
Normal file
197
banquise-website/lib/styles/designSystem.ts
Normal 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;
|
87
banquise-website/lib/utils/index.ts
Normal file
87
banquise-website/lib/utils/index.ts
Normal 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;
|
@ -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>
|
|
@ -1,9 +1,48 @@
|
|||||||
// Re-export types from their specific modules
|
// Re-export types from their specific modules
|
||||||
export type { Service } from './service';
|
export type { Service } from './service';
|
||||||
|
export type { Language, Translation } from './i18n';
|
||||||
|
|
||||||
|
// Types communs pour les composants UI
|
||||||
export interface AccordionItemProps {
|
export interface AccordionItemProps {
|
||||||
title: React.ReactNode;
|
title: React.ReactNode;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onToggle: () => void;
|
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';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user