Compare commits

...

17 Commits

Author SHA1 Message Date
81e63e5a8d fix service cards icons
Some checks failed
Build and Test / Classic Build (pull_request) Failing after 40s
Build and Test / Docker Build (pull_request) Has been skipped
2025-09-14 15:01:25 +02:00
796c7d1c21 optimize mobile navbar
Some checks failed
Build and Test / Classic Build (pull_request) Failing after 39s
Build and Test / Docker Build (pull_request) Has been skipped
2025-09-14 14:51:53 +02:00
0b6960085d fix Discord button
Some checks failed
Build and Test / Classic Build (pull_request) Failing after 51s
Build and Test / Docker Build (pull_request) Has been skipped
2025-09-14 14:48:24 +02:00
df15c7a838 check
Some checks failed
Build and Test / Classic Build (pull_request) Failing after 52s
Build and Test / Docker Build (pull_request) Has been skipped
2025-09-14 14:43:52 +02:00
7e47c6163d fix
Some checks failed
Build and Test / Classic Build (pull_request) Failing after 55s
Build and Test / Docker Build (pull_request) Has been skipped
2025-09-14 14:43:01 +02:00
57f5807876 optimize archi
Some checks failed
Build and Test / Classic Build (pull_request) Failing after 2m19s
Build and Test / Docker Build (pull_request) Has been skipped
2025-09-14 12:54:18 +02:00
8b374cf8c4 update CI
Some checks failed
Build / build-check (pull_request) Failing after 47s
2025-09-14 12:27:19 +02:00
54c51341a2 fix commit
Some checks failed
Build / build-check (pull_request) Failing after 58s
2025-09-14 12:11:00 +02:00
54d419d17a fix build
Some checks failed
Build / build-check (pull_request) Failing after 33s
2025-09-14 12:10:42 +02:00
ff63b5958a WIP: rework
Some checks failed
Build / build-check (pull_request) Failing after 33s
2025-09-14 11:53:12 +02:00
a63d9f4797 add new UI
Some checks failed
Build / build-check (pull_request) Failing after 1m30s
2025-09-14 11:24:59 +02:00
30fd66f2c9 Major UI update
Some checks failed
Build / build-check (pull_request) Failing after 1m33s
2025-09-13 22:55:24 +02:00
d36f6f48e8 update to Tailwind v4
Some checks failed
Build / build-check (pull_request) Failing after 57s
2025-09-13 22:26:20 +02:00
b32efb439c update archi & docker deploy process
Some checks failed
Build / build-check (pull_request) Failing after 1m18s
2025-09-13 19:15:45 +02:00
e1017e7570 clean
Some checks failed
Build / build-check (pull_request) Failing after 1m15s
2025-09-13 18:57:09 +02:00
545b7f9d91 archi cleaning
Some checks failed
Build / build-check (pull_request) Failing after 1m46s
2025-09-13 18:51:09 +02:00
537c2b01d1 init new version
Some checks failed
Build / build-check (pull_request) Failing after 2m2s
2025-09-13 18:23:10 +02:00
104 changed files with 7699 additions and 7916 deletions

View File

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

1
.gitignore vendored
View File

@ -8,6 +8,7 @@
# dotenv environment files (si tu en as)
.env
.env.*.local
.next
# system files
.DS_Store

View File

@ -1,66 +1,47 @@
# Website Front pour Banquise
Ce projet est une application web React développée avec Vite, TypeScript et TailwindCSS.
## Architecture du Projet
```
website-front/
├── banquise-website/ # Application React principale
│ ├── public/ # Fichiers statiques
│ ├── src/ # Code source
│ │ ├── assets/ # Images et ressources
│ │ ├── App.tsx # Composant principal
│ │ └── main.tsx # Point d'entrée de l'application
│ ├── index.html # Template HTML principal
│ ├── package.json # Configuration des dépendances
│ ├── tsconfig.json # Configuration TypeScript
│ ├── vite.config.ts # Configuration Vite
│ └── tailwind.config.js # Configuration TailwindCSS
└── shell.nix # Configuration pour environnement Nix
```
## Technologies Utilisées
- **React 18** - Bibliothèque d'interface utilisateur
- **TypeScript** - Langage de programmation typé
- **Vite** - Outil de build et serveur de développement
- **TailwindCSS** - Framework CSS utilitaire
- **React Router** - Navigation entre les pages
- **Zustand** - Gestion d'état
- **React Query** - Gestion des requêtes API
- **Framer Motion** - Animations
Depôt officiel du site internet de l'association "La Banquise" développé avec Next.js
## Pré-requis
- Node.js (v16.0.0 ou supérieur)
- npm ou yarn
- Node.js (v18+ recommandé)
- `pnpm` (utilisé pour le gestionnaire de paquets)
## Installation
## Utilisation
### Mode développement
Naviguer à la racine du site internet
```bash
# Se déplacer dans le dossier du projet
cd banquise-website
# Installer les dépendances
npm install
# ou avec yarn
yarn
```
## Scripts Disponibles
- `npm run dev` - Lance le serveur de développement
- `npm run build` - Compile le projet pour la production
- `npm run preview` - Prévisualise la version de production localement
- `npm run lint` - Vérifie la qualité du code avec ESLint
## Déploiement
### Compilation pour la Production
Installer toutes les dépendences nécéssaires
```bash
npm run build
pnpm install
```
Cette commande générera un dossier `dist` dans le répertoire `banquise-website/` contenant tous les fichiers optimisés pour la production.
Executer le serveur de developpement
```bash
pnpm dev
```
Acceder au site internet
```bash
http://localhost:3000
```
### Mode production
Naviguer à la racine du projet
```bash
cd banquise-website
```
Build l'image Docker
```bash
docker build -t banquise-website .
```
Executer l'image Docker en mode detach
```bash
docker run -p 3000:3000 -d banquise-website
```

View File

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

View File

@ -0,0 +1,38 @@
# Stage 1: install deps and build
FROM node:20-alpine AS builder
WORKDIR /app
# Install pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate
# Copy manifests
COPY package.json pnpm-lock.yaml* ./
# Install deps
RUN pnpm install --prod=false
# Copy source
COPY . .
# Build
RUN pnpm build
# Stage 2: production image
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
# Install pnpm runtime
RUN corepack enable && corepack prepare pnpm@latest --activate
# Copy only production files
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["pnpm", "start"]

View File

@ -1,54 +0,0 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default tseslint.config({
extends: [
// Remove ...tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked,
],
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default tseslint.config({
plugins: {
// Add the react-x and react-dom plugins
'react-x': reactX,
'react-dom': reactDom,
},
rules: {
// other rules...
// Enable its recommended typescript rules
...reactX.configs['recommended-typescript'].rules,
...reactDom.configs.recommended.rules,
},
})
```

View File

@ -0,0 +1,126 @@
@import "tailwindcss";
/* Configuration Tailwind v4 via CSS custom properties - Variables globales */
@layer base {
:root {
/* Polices */
--font-heading: 'Dela Gothic One', sans-serif;
--font-body: 'Roboto', sans-serif;
/* Couleurs personnalisées La Banquise */
--color-banquise-blue: 64, 180, 255;
--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;
/* Couleurs Discord officielles */
--color-discord-blurple: #5865F2;
--color-discord-dark: #4752C4;
--color-discord-old-blurple: #7289DA;
--color-discord-old-dark: #5B6EAE;
/* Transitions communes */
--transition-default: all 0.3s ease-in-out;
--transition-fast: all 0.2s ease-in-out;
/* Spacing commun */
--spacing-navbar: 4rem;
}
}
/* Minimal, valid utility helpers avec variables optimisées */
@layer utilities {
/* Text colors */
.text-banquise-blue { color: var(--color-banquise-blue-hex); }
.text-banquise-blue-dark { color: var(--color-banquise-blue-dark-hex); }
.text-banquise-blue-light { color: var(--color-banquise-blue-light-hex); }
.text-banquise-blue-lightest { color: var(--color-banquise-blue-lightest-hex); }
.text-banquise-gray { color: var(--color-banquise-gray); }
.text-discord { color: var(--color-discord-blurple); }
/* Background colors */
.bg-banquise-blue { background-color: var(--color-banquise-blue-hex); }
.bg-banquise-blue-dark { background-color: var(--color-banquise-blue-dark-hex); }
.bg-banquise-blue-light { background-color: var(--color-banquise-blue-light-hex); }
.bg-banquise-blue-lightest { background-color: var(--color-banquise-blue-lightest-hex); }
.bg-banquise-gray { background-color: var(--color-banquise-gray); }
.bg-discord { background-color: var(--color-discord-blurple); }
/* Opacity helpers using rgba() */
.bg-banquise-blue-5 { background-color: rgba(var(--color-banquise-blue), 0.05); }
.bg-banquise-blue-10 { background-color: rgba(var(--color-banquise-blue), 0.10); }
.bg-banquise-blue-20 { background-color: rgba(var(--color-banquise-blue), 0.20); }
.bg-banquise-blue-dark-10 { background-color: rgba(var(--color-banquise-blue-dark), 0.10); }
/* Border colors */
.border-banquise-blue { border-color: var(--color-banquise-blue-hex); }
.border-banquise-blue-lightest-30 { border-color: rgba(var(--color-banquise-blue-lightest), 0.3); }
/* Gradients shortcuts */
.from-banquise-blue { --tw-gradient-from: var(--color-banquise-blue-hex); }
.from-banquise-blue-dark { --tw-gradient-from: var(--color-banquise-blue-dark-hex); }
.via-banquise-blue { --tw-gradient-via: var(--color-banquise-blue-hex); }
.to-banquise-blue { --tw-gradient-to: var(--color-banquise-blue-hex); }
/* Shadow helpers */
.shadow-banquise-blue-20 { box-shadow: 0 4px 6px -1px rgba(var(--color-banquise-blue), 0.20); }
/* Transitions communes */
.transition-default { transition: var(--transition-default); }
.transition-fast { transition: var(--transition-fast); }
}
/* Animations optimisées */
@keyframes gentle-float {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-15px) rotate(1deg); }
}
@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; }
/* Configuration globale optimisée */
@media (prefers-reduced-motion: reduce) {
.animate-gentle-float,
.animate-ping,
.animate-pulse {
animation: none !important;
}
}
html { scroll-behavior: smooth; }
body { overflow-x: hidden; }
/* Scrollbar unifié pour tous les éléments */
.custom-scrollbar,
.popup-content {
scrollbar-width: thin;
scrollbar-color: rgba(31,93,137,0.3) transparent;
}
.custom-scrollbar::-webkit-scrollbar,
.popup-content::-webkit-scrollbar { width: 6px; }
.custom-scrollbar::-webkit-scrollbar-track,
.popup-content::-webkit-scrollbar-track { background: transparent; }
.custom-scrollbar::-webkit-scrollbar-thumb,
.popup-content::-webkit-scrollbar-thumb {
background: rgba(31,93,137,0.3);
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover,
.popup-content::-webkit-scrollbar-thumb:hover {
background: rgba(31,93,137,0.5);
}

View File

@ -0,0 +1,18 @@
import './globals.css'
import type { ReactNode } from 'react'
export const metadata = {
title: 'La Banquise - Hébergement et Communauté Tech',
description: "Association d'hébergement et lab réseau pour tous les étudiants et associations de l'EPITA. Services Wiki, Gitea, Panel de jeux.",
}
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="fr">
<head />
<body>
{children}
</body>
</html>
)
}

View File

@ -0,0 +1,108 @@
"use client"
import React from 'react';
import { ModernNavigation } from '@/components/layout/ModernNavigation';
import { HeroSection } from '@/components/sections/HeroSection';
import { ServicesSection } from '@/components/sections/ServicesSection';
import { TechFeaturesSection } from '@/components/sections/TechFeaturesSection';
import { AboutSection } from '@/components/sections/AboutSection';
import { Footer } from '@/components/layout/Footer';
import { Popup } from '@/components/ui/Popup';
import { ScrollToTopButton } from '@/components/ui/ScrollToTopButton';
import { ModernLanguageSwitcher } from '@/components/ui/ModernLanguageSwitcher';
import { OceanBackground } from '@/components/ui/OceanBackground';
import { useTranslation } from '@/lib/hooks/useTranslation';
import { useServiceModal } from '@/lib/hooks/useServiceModal';
import { useAccordion } from '@/lib/hooks/useAccordion';
export default function HomePage() {
const { t, currentLanguage, changeLanguage, availableLanguages } = useTranslation();
const { selectedService, openServiceModal, closeServiceModal } = useServiceModal();
const { openAccordion, toggleAccordion } = useAccordion();
return (
<div className="min-h-screen relative overflow-x-hidden">
{/* Navigation */}
<ModernNavigation
translations={t.navigation}
languageSwitcher={
<ModernLanguageSwitcher
currentLanguage={currentLanguage}
onLanguageChange={changeLanguage}
availableLanguages={availableLanguages}
/>
}
/>
{/* Hero Section avec motif de grille */}
<section
className="relative bg-grid-pattern"
style={{
background: 'linear-gradient(to bottom, var(--banquise-slate-50), var(--banquise-blue-50))',
backgroundImage: 'linear-gradient(rgba(31, 80, 120, 0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(31, 80, 120, 0.03) 1px, transparent 1px)',
backgroundSize: '30px 30px'
}}
>
<HeroSection translations={t.hero} commonTranslations={t.common} />
</section>
{/* Services Section avec transition subtile */}
<section
className="section-transition"
style={{ backgroundColor: 'var(--banquise-white)' }}
>
<div
className="py-4"
style={{
background: 'linear-gradient(to right, transparent, rgba(52, 166, 252, 0.1), transparent)'
}}
></div>
<ServicesSection
services={t.services}
onServiceClick={openServiceModal}
translations={t.common}
/>
</section>
{/* Tech Features Section avec motif de grille */}
<section
className="section-transition"
style={{
backgroundColor: 'var(--banquise-slate-50)',
backgroundImage: 'linear-gradient(rgba(31, 80, 120, 0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(31, 80, 120, 0.03) 1px, transparent 1px)',
backgroundSize: '30px 30px'
}}
>
<TechFeaturesSection />
</section>
{/* About Section avec transition moderne */}
<section
className="section-transition"
style={{ backgroundColor: 'var(--banquise-white)' }}
>
<div
className="py-4"
style={{
background: 'linear-gradient(to right, transparent, rgba(52, 166, 252, 0.08), transparent)'
}}
></div>
<AboutSection
openAccordion={openAccordion}
toggleAccordion={toggleAccordion}
/>
</section> {/* Footer - Même couleur que navbar */}
<Footer />
{/* UI Components */}
<ScrollToTopButton />
{selectedService && (
<Popup
service={selectedService}
onClose={closeServiceModal}
translations={t.common}
/>
)}
</div>
);
}

View File

@ -1,2 +0,0 @@
npm install
npm run build

View File

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

View File

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

View File

@ -0,0 +1,177 @@
import React, { useState, useRef, useEffect } from 'react';
import Image from 'next/image';
import { useTranslation } from '@/lib/hooks/useTranslation';
import type { AutheliaUser } from '@/lib/services/auth';
// Fonction utilitaire simple pour combiner les classes
const cn = (...classes: (string | undefined | null | false)[]): string => {
return classes.filter(Boolean).join(' ');
};
interface UserProfileProps {
user: AutheliaUser;
onLogout: () => void;
className?: string;
}
export const UserProfile: React.FC<UserProfileProps> = ({
user,
onLogout,
className,
}) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const { t } = useTranslation();
const defaultAvatarSmall = `https://ui-avatars.com/api/?name=${encodeURIComponent(user.name)}&background=0ea5e9&color=fff&size=32`;
const defaultAvatarLarge = `https://ui-avatars.com/api/?name=${encodeURIComponent(user.name)}&background=0ea5e9&color=fff&size=40`;
const avatarSrc = user.avatar ?? defaultAvatarSmall;
const avatarLargeSrc = user.avatar ?? defaultAvatarLarge;
// Fermer la popup en cliquant à l'extérieur
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
buttonRef.current &&
!dropdownRef.current.contains(event.target as Node) &&
!buttonRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
const handleLogout = async () => {
setIsOpen(false);
onLogout();
};
return (
<div className={cn('relative', className)}>
{/* Bouton Avatar */}
<button
ref={buttonRef}
onClick={() => setIsOpen(!isOpen)}
className={cn(
'flex items-center justify-center w-8 h-8 rounded-full overflow-hidden',
'border-2 border-transparent hover:border-blue-300',
'transition-all duration-200',
'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
isOpen && 'ring-2 ring-blue-500 ring-offset-2'
)}
aria-label={t.user.userMenu}
>
<Image
src={avatarSrc}
alt={`Avatar de ${user.name}`}
className="w-full h-full object-cover rounded-full"
width={32}
height={32}
/>
</button>
{/* Popup Menu */}
{isOpen && (
<div
ref={dropdownRef}
className={cn(
'absolute right-0 top-full mt-2 w-64 bg-white rounded-lg shadow-lg',
'border border-gray-200 z-50',
'transform opacity-0 scale-95 animate-in fade-in-0 zoom-in-95',
'duration-200'
)}
style={{
animation: 'fadeInScale 0.2s ease-out forwards',
}}
>
{/* Header avec informations utilisateur */}
<div className="px-4 py-3 border-b border-gray-200">
<div className="flex items-center space-x-3">
<Image
src={avatarLargeSrc}
alt={`Avatar de ${user.name}`}
className="w-10 h-10 rounded-full object-cover"
width={40}
height={40}
/>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">
{user.name}
</p>
<p className="text-xs text-gray-500 truncate">
{user.email}
</p>
</div>
</div>
</div>
{/* Groupes utilisateur (si disponibles) */}
{user.groups && user.groups.length > 0 && (
<div className="px-4 py-3 border-b border-gray-200">
<p className="text-xs font-medium text-gray-700 mb-2">{t.user.groups}</p>
<div className="flex flex-wrap gap-1">
{user.groups.map((group, index) => (
<span
key={index}
className={cn(
'inline-flex px-2 py-1 text-xs font-medium rounded',
'bg-blue-100 text-blue-800'
)}
>
{group}
</span>
))}
</div>
</div>
)}
{/* Actions */}
<div className="py-2">
<button
onClick={handleLogout}
className={cn(
'w-full px-4 py-2 text-left text-sm text-gray-700',
'hover:bg-gray-100 transition-colors duration-150',
'flex items-center space-x-2'
)}
>
<svg
className="w-4 h-4 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
<span>{t.user.logout}</span>
</button>
</div>
</div>
)}
<style jsx>{`
@keyframes fadeInScale {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
`}</style>
</div>
);
};

View File

@ -0,0 +1,151 @@
import React from 'react';
import { URLS, SITE_CONFIG } from '@/lib/config/constants';
import { BookOpen, GitBranch, Gamepad2, Cloud, Rocket, Heart } from 'lucide-react';
import { useTranslation } from '@/lib/hooks/useTranslation';
import { cn, commonClasses } from '@/lib/utils';
import { DiscordIcon } from '@/components/ui/DiscordLogo';
const EmailIcon = () => (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
);
// Composant pour les liens de service factorisant la structure répétitive
interface ServiceLinkProps {
href: string;
icon: React.ComponentType<any>;
children: React.ReactNode;
}
const ServiceLink: React.FC<ServiceLinkProps> = ({ href, icon: Icon, children }) => (
<a
href={href}
className={cn(
'flex items-center text-gray-300 hover:text-blue-400',
commonClasses.transition,
'hover:translate-x-2 transform'
)}
>
<Icon className="w-5 h-5 mr-3" strokeWidth={2} />
{children}
</a>
);
// Composant pour les boutons sociaux factorisant la structure répétitive
interface SocialButtonProps {
href: string;
label: string;
children: React.ReactNode;
}
const SocialButton: React.FC<SocialButtonProps> = ({ href, label, children }) => (
<a
href={href}
className={cn(
'w-12 h-12 bg-gray-800 hover:bg-blue-600 rounded-xl',
'flex items-center justify-center',
commonClasses.transition,
commonClasses.hoverScale,
'shadow-lg hover:shadow-blue-500/25'
)}
aria-label={label}
>
{children}
</a>
);
export const Footer: React.FC = () => {
const { t } = useTranslation();
return (
<footer className="bg-gray-900 text-white py-16 px-4 sm:px-6 lg:px-8 border-t border-gray-800">
<div className="max-w-7xl mx-auto">
{/* Contenu principal du footer */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-12 mb-12">
{/* Marque et description */}
<div className="space-y-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center shadow-lg">
<span className="text-white font-bold text-xl">B</span>
</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>
{/* Liens rapides services */}
<div className="space-y-6">
<h3 className="text-white font-bold text-lg mb-6">{t.footer.ourServices}</h3>
<div className="space-y-4">
<ServiceLink href={URLS.services.wiki} icon={BookOpen}>
Wiki
</ServiceLink>
<ServiceLink href={URLS.services.gitea} icon={GitBranch}>
Gitea
</ServiceLink>
<ServiceLink href={URLS.services.panel} icon={Gamepad2}>
{t.footer.gamingPanel}
</ServiceLink>
<ServiceLink href={URLS.services.opencloud} icon={Cloud}>
OpenCloud
</ServiceLink>
</div>
</div>
{/* Informations communauté */}
<div className="space-y-6">
<h3 className="text-white font-bold text-lg mb-6">{t.footer.community}</h3>
<div className="bg-gradient-to-r from-blue-900/30 to-blue-800/30 rounded-xl p-6 border border-blue-800/30">
<h4 className="text-blue-400 font-semibold mb-2">{t.footer.joinAssociation}</h4>
<p className="text-gray-300 text-sm mb-4">
{t.footer.joinDescription}
</p>
<a
href={URLS.social.discord}
className={cn(
'inline-flex items-center text-blue-400 hover:text-blue-300',
'text-sm font-semibold',
commonClasses.transition
)}
>
<Rocket className="w-4 h-4 mr-2" strokeWidth={2} />
{t.footer.joinNow}
</a>
</div>
</div>
</div>
{/* Barre du bas */}
<div className="flex flex-col md:flex-row justify-between items-center gap-6 pt-8 border-t border-gray-800">
<p className="text-gray-400 text-sm text-center md:text-left">
© 2025 {SITE_CONFIG.name}. {t.footer.copyright}
</p>
<div className="flex items-center gap-6 text-sm text-gray-400">
<span className="flex items-center">
{t.footer.madeWith}
<Heart className="text-red-500 mx-1 w-4 h-4" strokeWidth={2} fill="currentColor" />
{t.footer.by}
</span>
<div className="w-1 h-1 bg-gray-600 rounded-full"></div>
<span className="text-blue-400 font-semibold">EPITA 2025</span>
</div>
</div>
</div>
</footer>
);
};

View File

@ -0,0 +1,160 @@
import React, { useEffect } from 'react';
import { Logo } from './navbar/Logo';
import { useTranslation } from '@/lib/hooks/useTranslation';
import { URLS } from '@/lib/config/constants';
import { cn, createNavClickHandler } from '@/lib/utils';
import { DiscordLogo } from '@/components/ui/DiscordLogo';
import type { Translation } from '@/types/i18n';
interface MobileMenuProps {
isOpen: boolean;
onClose: () => void;
translations: Translation['navigation'];
}
interface MobileNavItemProps {
title: string;
href: string;
isExternal?: boolean;
onClick?: () => void;
isDiscord?: boolean;
}
const MobileNavItem: React.FC<MobileNavItemProps> = ({
title,
href,
isExternal = false,
onClick,
isDiscord = false
}) => {
const handleClick = (e: React.MouseEvent) => {
if (onClick) {
e.preventDefault();
onClick();
}
};
return (
<a
href={href}
onClick={handleClick}
className={cn(
'flex items-center justify-between px-4 py-3 rounded-lg',
'text-white hover:bg-white/10 transition-colors duration-200',
'border-b border-white/10 last:border-b-0'
)}
target={isExternal ? '_blank' : undefined}
rel={isExternal ? 'noopener noreferrer' : undefined}
>
<div className="flex items-center space-x-3">
{isDiscord && <DiscordLogo size="sm" className="text-[#5865F2]" />}
<span className="font-medium">{title}</span>
</div>
{isExternal && (
<svg className="w-4 h-4 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
)}
</a>
);
};
export const MobileMenu: React.FC<MobileMenuProps> = ({ isOpen, onClose, translations }) => {
const { t } = useTranslation();
// Gérer le scroll du body
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'unset';
}
return () => {
document.body.style.overflow = 'unset';
};
}, [isOpen]);
// Gestionnaire de navigation
const handleNavClick = createNavClickHandler(onClose);
return (
<div className={cn(
'md:hidden fixed inset-0 z-[100] transition-opacity duration-300',
isOpen ? 'visible opacity-100' : 'invisible opacity-0'
)}>
{/* Overlay simple */}
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
onClick={onClose}
/>
{/* Menu Panel épuré */}
<div className={cn(
'absolute top-0 right-0 h-full w-72 max-w-[85vw]',
'bg-blue-900/95 backdrop-blur-lg shadow-xl',
'border-l border-white/10',
'transition-transform duration-300 ease-out',
isOpen ? 'translate-x-0' : 'translate-x-full'
)}>
{/* Header simple */}
<div className="flex items-center justify-between p-4 border-b border-white/10">
<Logo scrolled={false} />
<button
className="p-2 rounded-lg hover:bg-white/10 transition-colors"
onClick={onClose}
aria-label="Fermer le menu"
>
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Navigation simple */}
<div className="p-4 space-y-2">
<MobileNavItem
title={translations.home}
href="#home"
onClick={() => handleNavClick('home')}
/>
<MobileNavItem
title={translations.services}
href="#services"
onClick={() => handleNavClick('services')}
/>
<MobileNavItem
title={translations.about}
href="#about"
onClick={() => handleNavClick('about')}
/>
<MobileNavItem
title={translations.contact}
href="mailto:contact@la-banquise.fr"
onClick={() => handleNavClick('contact')}
/>
{/* Séparateur simple */}
<div className="border-t border-white/10 my-4" />
<MobileNavItem
title="Discord"
href={URLS.social.discord}
isExternal={true}
isDiscord={true}
/>
<MobileNavItem
title="Se connecter"
href={URLS.services.auth}
isExternal={true}
/>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,73 @@
import React from 'react';
import { useScrollEffects } from '@/lib/hooks/useScrollEffects';
import { Logo } from './navbar/Logo';
import { NavLinks } from './navbar/NavLinks';
import { ActionButtons } from './navbar/ActionButtons';
import { MobileMenuButton } from './navbar/MobileMenuButton';
import { MobileMenu } from './MobileMenu';
import { cn, useResizeHandler } from '@/lib/utils';
import type { Translation } from '@/types/i18n';
interface ModernNavigationProps {
translations: Translation['navigation'];
languageSwitcher: React.ReactElement;
}
export const ModernNavigation: React.FC<ModernNavigationProps> = ({
translations,
languageSwitcher
}) => {
const { scrolled } = useScrollEffects();
const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);
// Fermer le menu mobile lors du redimensionnement - optimisé
React.useEffect(() => {
const cleanup = useResizeHandler(() => setMobileMenuOpen(false));
return cleanup;
}, []);
return (
<>
{/* Navigation moderne épurée */}
<nav className={cn(
'fixed top-0 left-0 right-0 z-50',
'bg-blue-700/90 backdrop-blur-md border-b border-blue-600/20',
'transition-all duration-200',
scrolled && 'shadow-md'
)}>
<div className="max-w-7xl mx-auto">
<div className="flex justify-between items-center px-4 sm:px-6 lg:px-8 h-14 md:h-16">
<Logo scrolled={scrolled} />
<div className="flex-1 flex justify-center">
<NavLinks translations={translations} scrolled={scrolled} />
</div>
<ActionButtons
scrolled={scrolled}
languageSwitcher={languageSwitcher}
/>
<MobileMenuButton
isOpen={mobileMenuOpen}
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
/>
</div>
</div>
{/* Ligne de séparation moderne */}
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-blue-400/30 to-transparent" />
</nav>
{/* Spacer pour compenser la navbar fixed */}
<div className="h-14 md:h-16" />
<MobileMenu
isOpen={mobileMenuOpen}
onClose={() => setMobileMenuOpen(false)}
translations={translations}
/>
</>
);
};

View File

@ -0,0 +1,28 @@
import React from 'react';
import { DiscordButton } from '@/components/ui/DiscordButton';
interface ActionButtonsProps {
scrolled: boolean;
languageSwitcher: React.ReactElement;
}
export const ActionButtons: React.FC<ActionButtonsProps> = ({
scrolled,
languageSwitcher
}) => {
return (
<div className="hidden md:flex items-center space-x-3">
{/* Language Switcher */}
{languageSwitcher}
{/* Discord Button */}
<DiscordButton
size="sm"
className="transition-all duration-200 hover:scale-105"
showIcon={true}
>
Discord
</DiscordButton>
</div>
);
};

View File

@ -0,0 +1,64 @@
import React from 'react';
import Image from 'next/image';
import { SITE_CONFIG } from '@/lib/config/constants';
// Fonction utilitaire simple pour combiner les classes
const mergeClasses = (...classes: (string | undefined | null | false)[]): string => {
return classes.filter(Boolean).join(' ');
};
interface LogoProps {
scrolled?: boolean;
className?: string;
}
export const Logo: React.FC<LogoProps> = ({ scrolled = false, className }) => {
return (
<div className={mergeClasses(
'flex items-center group cursor-pointer transition-all duration-200',
className
)}>
{/* Logo moderne avec effet subtil */}
<div className="relative flex items-center">
{/* Effet de glow moderne et subtil */}
<div className="absolute inset-0 bg-blue-400/20 rounded-2xl blur-lg opacity-0 group-hover:opacity-100 transition-all duration-300 scale-125"></div>
{/* Container du logo */}
<div className={mergeClasses(
'relative flex items-center justify-center rounded-2xl transition-all duration-200',
'bg-white/10 backdrop-blur-sm border border-white/20',
'group-hover:bg-white/15 group-hover:scale-[1.02] group-hover:border-white/30',
scrolled ? 'p-2' : 'p-2.5'
)}>
<Image
src="/assets/banquise_server.svg"
alt={SITE_CONFIG.name}
width={scrolled ? 28 : 32}
height={scrolled ? 28 : 32}
className="transition-all duration-200 group-hover:scale-105"
style={{
filter: 'drop-shadow(0 2px 8px rgba(59, 130, 246, 0.3))'
}}
/>
</div>
</div>
{/* Texte de marque épuré */}
<div className="ml-3 hidden sm:block">
<h1 className={mergeClasses(
'font-semibold text-white tracking-tight transition-all duration-200',
scrolled ? 'text-base' : 'text-lg lg:text-xl',
'group-hover:text-blue-100'
)}>
{SITE_CONFIG.name}
</h1>
{!scrolled && (
<p className="text-xs text-white/70 font-medium transition-all duration-200 group-hover:text-white/90">
{SITE_CONFIG.tagline}
</p>
)}
</div>
</div>
);
};

View File

@ -1,5 +1,9 @@
import React from 'react';
import { mergeClasses as cn } from '../../../styles/designSystem';
// Fonction utilitaire simple pour combiner les classes
const cn = (...classes: (string | undefined | null | false)[]): string => {
return classes.filter(Boolean).join(' ');
};
interface MobileMenuButtonProps {
isOpen: boolean;
@ -19,7 +23,7 @@ export const MobileMenuButton: React.FC<MobileMenuButtonProps> = ({
'bg-white/10 hover:bg-white/20 active:bg-white/25',
'border border-white/20 hover:border-white/30',
'hover:scale-105 active:scale-95',
'focus:outline-none focus:ring-2 focus:ring-banquise-blue-light/50',
'focus:outline-none focus:ring-2 focus:ring-blue-400/50',
className
)}
onClick={onClick}
@ -43,7 +47,7 @@ export const MobileMenuButton: React.FC<MobileMenuButtonProps> = ({
</div>
{/* Subtle glow effect on hover */}
<div className="absolute inset-0 rounded-xl bg-gradient-to-r from-banquise-blue-light/20 to-banquise-blue/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="absolute inset-0 rounded-xl bg-gradient-to-r from-blue-400/20 to-blue-600/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
</button>
);
};

View File

@ -0,0 +1,144 @@
import React from 'react';
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 NavLinksProps {
translations: Translation['navigation'];
scrolled?: boolean;
className?: string;
}
interface NavLinkProps {
href: string;
children: React.ReactNode;
isActive?: boolean;
onClick?: () => void;
}
const NavLink: React.FC<NavLinkProps> = ({ href, children, isActive = false, onClick }) => {
return (
<a
href={href}
onClick={(e) => {
e.preventDefault();
onClick?.();
}}
className={mergeClasses(
// Base styles
'relative px-4 py-2.5 text-sm font-medium transition-all duration-200 rounded-xl',
'focus:outline-none focus:ring-2 focus:ring-blue-400/50',
// États conditionnels
isActive
? 'text-white bg-white/15 shadow-sm backdrop-blur-sm border border-white/20'
: 'text-white/80 hover:text-white hover:bg-white/10'
)}
>
<span className="relative z-10">{children}</span>
{/* Indicateur actif moderne */}
{isActive && (
<div className="absolute bottom-0 left-1/2 transform -translate-x-1/2 w-1 h-1 bg-blue-300 rounded-full" />
)}
</a>
);
};
export const NavLinks: React.FC<NavLinksProps> = ({ translations, className }) => {
const [activeSection, setActiveSection] = React.useState<string>('home');
// Observer pour détecter la section active (simplifié)
React.useEffect(() => {
const handleScroll = () => {
const scrollPosition = window.scrollY;
// Si on est en haut de la page
if (scrollPosition < 200) {
setActiveSection('home');
return;
}
// Détection des sections
const sections = ['services', 'about'];
let currentSection = 'home';
sections.forEach((sectionId) => {
const element = document.getElementById(sectionId);
if (element) {
const rect = element.getBoundingClientRect();
if (rect.top <= 200 && rect.bottom >= 200) {
currentSection = sectionId;
}
}
});
setActiveSection(currentSection);
};
window.addEventListener('scroll', handleScroll, { passive: true });
handleScroll();
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const handleNavClick = (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) {
const navHeight = 64; // Hauteur de la navbar
const elementPosition = element.offsetTop - navHeight;
window.scrollTo({
top: elementPosition,
behavior: 'smooth'
});
}
}
};
return (
<nav className={mergeClasses(
'hidden md:flex items-center space-x-2',
className
)}>
<NavLink
href="#home"
isActive={activeSection === 'home'}
onClick={() => handleNavClick('home')}
>
{translations.home}
</NavLink>
<NavLink
href="#services"
isActive={activeSection === 'services'}
onClick={() => handleNavClick('services')}
>
{translations.services}
</NavLink>
<NavLink
href="#about"
isActive={activeSection === 'about'}
onClick={() => handleNavClick('about')}
>
{translations.about}
</NavLink>
<NavLink
href="mailto:contact@la-banquise.fr"
isActive={false}
onClick={() => handleNavClick('contact')}
>
{translations.contact}
</NavLink>
</nav>
);
};

View File

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

View File

@ -0,0 +1,148 @@
import React from 'react';
import Image from 'next/image';
import { ArrowRight } from 'lucide-react';
import { cn, commonClasses } from '@/lib/utils';
import type { Translation } from '@/types/i18n';
interface HeroSectionProps {
translations: Translation['hero'];
commonTranslations: Translation['common'];
}
// Composant pour les boutons CTA factorisant la structure répétitive
interface CTAButtonProps {
href: string;
primary?: boolean;
children: React.ReactNode;
icon?: React.ReactNode;
}
const CTAButton: React.FC<CTAButtonProps> = ({ href, primary = false, children, icon }) => {
const handleClick = (e: React.MouseEvent) => {
e.preventDefault();
const targetId = href.replace('#', '');
if (targetId === 'services' || targetId === 'about') {
document.getElementById(targetId)?.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
};
const baseClasses = cn(
'inline-flex items-center justify-center',
'text-lg font-bold rounded-2xl',
commonClasses.transition,
commonClasses.hoverScale,
'active:scale-95'
);
const primaryClasses = cn(
'px-12 py-5 text-white',
'bg-gradient-to-r from-blue-600 to-blue-500',
'shadow-2xl hover:shadow-blue-500/50',
'hover:-translate-y-2 border-2 border-blue-600/20',
'group relative overflow-hidden'
);
const secondaryClasses = cn(
'px-8 py-4 text-blue-700 bg-white',
'border-2 border-blue-600 shadow-lg',
'hover:shadow-xl hover:bg-blue-50'
);
return (
<a
href={href}
onClick={handleClick}
className={cn(baseClasses, primary ? primaryClasses : secondaryClasses)}
>
{primary && (
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 rounded-2xl" />
)}
<span className="relative z-10">{children}</span>
{icon && <span className="relative z-10 ml-3">{icon}</span>}
</a>
);
};
export const HeroSection: React.FC<HeroSectionProps> = ({ translations, commonTranslations }) => (
<section
id="home"
className="min-h-screen flex flex-col justify-center items-center text-center relative px-4 sm:px-6 lg:px-8 bg-gradient-to-br from-gray-50 via-blue-50/30 to-gray-100"
>
{/* Motifs de fond optimisés - factorisation des styles répétitifs */}
<div
className="absolute inset-0 opacity-40"
style={{
backgroundImage: 'radial-gradient(circle at 1px 1px, rgba(59,130,246,0.5) 1px, transparent 0)',
backgroundSize: '32px 32px'
}}
/>
<div
className="absolute inset-0 opacity-20"
style={{
backgroundImage: 'radial-gradient(circle at 1px 1px, rgba(99,102,241,0.4) 1px, transparent 0)',
backgroundSize: '64px 64px'
}}
/>
{/* Contenu principal */}
<div className="relative z-10 max-w-5xl mx-auto">
{/* Logo principal avec effet moderne */}
<div className="mb-16 group">
<div className="relative inline-block">
<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={cn(
'relative bg-white/90 backdrop-blur-lg rounded-3xl p-10',
'border border-blue-200/50 shadow-2xl',
commonClasses.transition,
'group-hover:shadow-blue-200/50 group-hover:shadow-3xl',
commonClasses.hoverScale
)}>
<Image
src="/assets/banquise_server.svg"
alt={translations.title}
width={140}
height={140}
className={cn(
'w-28 h-28 md:w-32 md:h-32 lg:w-36 lg:h-36',
'transition-transform duration-500 group-hover:scale-110'
)}
style={{
filter: 'drop-shadow(0 8px 24px rgba(59, 130, 246, 0.4))'
}}
/>
</div>
</div>
</div>
{/* Titre principal avec gradient moderne */}
<h1 className="text-4xl md:text-5xl lg:text-7xl font-bold leading-tight tracking-tight mb-8 bg-gradient-to-r from-gray-900 via-blue-700 to-gray-900 bg-clip-text text-transparent">
{translations.title}
</h1>
{/* Sous-titre avec contraste amélioré */}
<p className="text-lg md:text-xl lg:text-2xl text-gray-700 mx-auto max-w-3xl mb-14 leading-relaxed font-medium">
{translations.subtitle}
</p>
{/* Call-to-action optimisé */}
<div className="flex flex-col sm:flex-row gap-6 justify-center items-center mb-20">
<CTAButton
href="#services"
primary
icon={<ArrowRight className="w-6 h-6 transition-transform duration-300 group-hover:translate-x-2" strokeWidth={2.5} />}
>
{translations.cta}
</CTAButton>
<CTAButton href="#about">
{commonTranslations.learnMore}
</CTAButton>
</div>
</div>
</section>
);

View File

@ -0,0 +1,47 @@
import React from 'react';
import { ServiceCard } from '@/components/common/ServiceCard';
import { SectionHeader } from '@/components/ui/SectionHeader';
import { useTranslation } from '@/lib/hooks/useTranslation';
import type { Service } from '@/types/service';
interface ServicesSectionProps {
services: Service[];
onServiceClick: (service: Service) => void;
translations: {
discoverFeatures: string;
};
}
export const ServicesSection: React.FC<ServicesSectionProps> = ({
services,
onServiceClick,
translations
}) => {
const { t } = useTranslation();
return (
<section
id="services"
className="py-24 md:py-32 px-4 sm:px-6 lg:px-8 bg-gradient-to-br from-white via-blue-50/30 to-gray-50"
>
<div className="max-w-7xl mx-auto">
{/* Header de section moderne avec forte hiérarchie - factorisation */}
<SectionHeader
title={t.sections.ourServices}
subtitle={translations.discoverFeatures}
/>
{/* Grille de services avec espacement généreux */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-12">
{services.map((service) => (
<ServiceCard
key={service.name}
service={service}
onServiceClick={onServiceClick}
/>
))}
</div>
</div>
</section>
);
};

View File

@ -0,0 +1,89 @@
import React from 'react';
import { Rocket, Database, Globe, Shield } from 'lucide-react';
import { useTranslation } from '@/lib/hooks/useTranslation';
export const TechFeaturesSection: React.FC = () => {
const { t } = useTranslation();
return (
<section className="py-12 sm:py-16 md:py-20 relative z-2 w-full max-w-6xl mx-auto px-4 sm:px-6 md:px-8">
{/* Motif de fond avec grille visible */}
<div
className="absolute inset-0 opacity-30"
style={{
backgroundImage: 'radial-gradient(circle at 1px 1px, rgba(59,130,246,0.4) 1px, transparent 0)',
backgroundSize: '32px 32px'
}}
/>
{/* Grille secondaire pour la profondeur */}
<div
className="absolute inset-0 opacity-15"
style={{
backgroundImage: 'radial-gradient(circle at 1px 1px, rgba(99,102,241,0.3) 1px, transparent 0)',
backgroundSize: '64px 64px'
}}
/>
<div className="relative z-10">
<div className="w-20 h-1 bg-gradient-to-r from-blue-400 to-blue-600 mx-auto mb-6 sm:mb-8 rounded-full"></div>
<h2 className="text-gray-800 text-2xl sm:text-3xl md:text-4xl mb-4 sm:mb-6 text-center font-heading font-bold tracking-tight px-2" style={{ textShadow: '0 2px 4px rgba(0, 0, 0, 0.2)' }}>
{t.infrastructure.title}
</h2>
<p className="text-gray-700 text-lg sm:text-xl opacity-90 mb-12 sm:mb-14 md:mb-16 max-w-4xl text-center mx-auto leading-relaxed px-2" style={{ textShadow: '0 1px 3px rgba(0, 0, 0, 0.2)' }}>
{t.infrastructure.subtitle}
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4 sm:gap-6 w-full">
<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 transform hover:-translate-y-4 hover:scale-105">
<div className="absolute inset-0 bg-gradient-to-br from-blue-50/50 to-indigo-50/50 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="relative z-10 flex flex-col items-center text-center">
<div className="text-white bg-gradient-to-br from-blue-500 to-blue-600 w-16 h-16 sm:w-20 sm:h-20 flex items-center justify-center rounded-2xl shadow-lg group-hover:scale-110 transition-transform duration-300 mb-4 sm:mb-6">
<Rocket className="w-8 h-8 sm:w-10 sm:h-10" strokeWidth={2} />
</div>
<h3 className="text-lg sm:text-xl mb-3 sm:mb-4 text-gray-900 font-heading font-semibold group-hover:text-blue-700 transition-colors duration-300">{t.infrastructure.features.performance.title}</h3>
<p className="text-gray-600 leading-relaxed text-sm group-hover:text-gray-700 transition-colors duration-300">{t.infrastructure.features.performance.description}</p>
</div>
</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 transform hover:-translate-y-4 hover:scale-105">
<div className="absolute inset-0 bg-gradient-to-br from-blue-50/50 to-indigo-50/50 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="relative z-10 flex flex-col items-center text-center">
<div className="text-white bg-gradient-to-br from-blue-500 to-blue-600 w-16 h-16 sm:w-20 sm:h-20 flex items-center justify-center rounded-2xl shadow-lg group-hover:scale-110 transition-transform duration-300 mb-4 sm:mb-6">
<Database className="w-8 h-8 sm:w-10 sm:h-10" strokeWidth={2} />
</div>
<h3 className="text-lg sm:text-xl mb-3 sm:mb-4 text-gray-900 font-heading font-semibold group-hover:text-blue-700 transition-colors duration-300">{t.infrastructure.features.storage.title}</h3>
<p className="text-gray-600 leading-relaxed text-sm group-hover:text-gray-700 transition-colors duration-300">{t.infrastructure.features.storage.description}</p>
</div>
</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 transform hover:-translate-y-4 hover:scale-105">
<div className="absolute inset-0 bg-gradient-to-br from-blue-50/50 to-indigo-50/50 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="relative z-10 flex flex-col items-center text-center">
<div className="text-white bg-gradient-to-br from-blue-500 to-blue-600 w-16 h-16 sm:w-20 sm:h-20 flex items-center justify-center rounded-2xl shadow-lg group-hover:scale-110 transition-transform duration-300 mb-4 sm:mb-6">
<Globe className="w-8 h-8 sm:w-10 sm:h-10" strokeWidth={2} />
</div>
<h3 className="text-lg sm:text-xl mb-3 sm:mb-4 text-gray-900 font-heading font-semibold group-hover:text-blue-700 transition-colors duration-300">{t.infrastructure.features.network.title}</h3>
<p className="text-gray-600 leading-relaxed text-sm group-hover:text-gray-700 transition-colors duration-300">{t.infrastructure.features.network.description}</p>
</div>
</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 transform hover:-translate-y-4 hover:scale-105">
<div className="absolute inset-0 bg-gradient-to-br from-blue-50/50 to-indigo-50/50 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="relative z-10 flex flex-col items-center text-center">
<div className="text-white bg-gradient-to-br from-blue-500 to-blue-600 w-16 h-16 sm:w-20 sm:h-20 flex items-center justify-center rounded-2xl shadow-lg group-hover:scale-110 transition-transform duration-300 mb-4 sm:mb-6">
<Shield className="w-8 h-8 sm:w-10 sm:h-10" strokeWidth={2} />
</div>
<h3 className="text-lg sm:text-xl mb-3 sm:mb-4 text-gray-900 font-heading font-semibold group-hover:text-blue-700 transition-colors duration-300">{t.infrastructure.features.security.title}</h3>
<p className="text-gray-600 leading-relaxed text-sm group-hover:text-gray-700 transition-colors duration-300">{t.infrastructure.features.security.description}</p>
</div>
</div>
</div>
</div>
</section>
);
};

View File

@ -0,0 +1,24 @@
import React from 'react';
import type { AccordionItemProps } from '../../types';
export const AccordionItem: React.FC<AccordionItemProps> = ({ title, children, isOpen, onToggle }) => (
<div className={`group relative bg-white rounded-2xl shadow-lg border border-gray-200 transition-all duration-300 overflow-hidden ${isOpen ? 'shadow-2xl border-blue-300 scale-[1.01]' : ''} hover:shadow-xl hover:border-blue-300`}>
{/* Overlay effect on hover */}
<div className="absolute inset-0 bg-gradient-to-br from-blue-50/50 to-indigo-50/50 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none" />
<div
className="relative z-10 p-6 sm:p-8 cursor-pointer flex items-center justify-between font-semibold text-gray-900 transition-all duration-200 text-base sm:text-lg select-none hover:text-blue-700"
onClick={onToggle}
>
<div className="flex items-center flex-1 mr-4 font-heading">{title}</div>
<span className={`text-xl sm:text-2xl transition-transform duration-300 text-blue-600 flex-shrink-0 ${isOpen ? 'rotate-180' : ''}`}>
</span>
</div>
<div className={`relative z-10 transition-all duration-500 overflow-hidden ${isOpen ? 'max-h-[1000px] pb-6 px-6 sm:pb-8 sm:px-8' : 'max-h-0'}`}>
<div className="text-gray-700 leading-relaxed text-sm sm:text-base">
{children}
</div>
</div>
</div>
);

View File

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

View File

@ -0,0 +1,47 @@
import React from 'react';
import { Button } from '@/components/common/Button';
import { DiscordLogo } from './DiscordLogo';
import { URLS } from '@/lib/config/constants';
interface DiscordButtonProps {
children: React.ReactNode;
href?: string;
onClick?: () => void;
size?: 'sm' | 'md' | 'lg';
className?: string;
showIcon?: boolean;
}
export const DiscordButton: React.FC<DiscordButtonProps> = ({
children,
href = URLS.social.discord,
onClick,
size = 'md',
className = '',
showIcon = true
}) => {
const content = (
<>
{showIcon && <DiscordLogo size={size === 'sm' ? 'sm' : 'md'} className="text-white mr-2" />}
{children}
</>
);
if (href) {
return (
<a href={href} target="_blank" rel="noopener noreferrer">
<Button variant="discord" size={size} className={className}>
{content}
</Button>
</a>
);
}
return (
<Button variant="discord" size={size} onClick={onClick} className={className}>
{content}
</Button>
);
};
export default DiscordButton;

View File

@ -0,0 +1,62 @@
import React from 'react';
import Image from 'next/image';
interface DiscordLogoProps {
size?: 'sm' | 'md' | 'lg' | 'xl';
variant?: 'svg' | 'png';
className?: string;
alt?: string;
}
const sizeClasses = {
sm: 'w-4 h-4',
md: 'w-6 h-6',
lg: 'w-8 h-8',
xl: 'w-12 h-12'
};
// Logo Discord officiel en SVG
const DiscordSVG: React.FC<{ className?: string }> = ({ className }) => (
<svg
className={className}
viewBox="0 0 127.14 96.36"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="currentColor"
d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"
/>
</svg>
);
export const DiscordLogo: React.FC<DiscordLogoProps> = ({
size = 'md',
variant = 'svg',
className = '',
alt = 'Discord'
}) => {
const baseClasses = sizeClasses[size];
const finalClassName = `${baseClasses} ${className}`;
if (variant === 'png') {
return (
<Image
src="/assets/discord-mark-blurple.png"
alt={alt}
width={size === 'sm' ? 16 : size === 'md' ? 24 : size === 'lg' ? 32 : 48}
height={size === 'sm' ? 16 : size === 'md' ? 24 : size === 'lg' ? 32 : 48}
className={finalClassName}
/>
);
}
return <DiscordSVG className={finalClassName} />;
};
// Export du composant SVG pour usage direct
export const DiscordIcon = () => (
<DiscordLogo size="md" variant="svg" className="text-[#5865F2]" />
);
export default DiscordLogo;

View File

@ -1,6 +1,10 @@
import React, { useState } from 'react';
import { mergeClasses as cn } from '../../styles/designSystem';
import type { Language } from '../../types/i18n';
import React, { useState, useRef, useEffect } from 'react';
import type { Language } from '@/types/i18n';
// Fonction utilitaire simple pour combiner les classes
const cn = (...classes: (string | undefined | null | false)[]): string => {
return classes.filter(Boolean).join(' ');
};
interface ModernLanguageSwitcherProps {
currentLanguage: Language;
@ -14,6 +18,32 @@ export const ModernLanguageSwitcher: React.FC<ModernLanguageSwitcherProps> = ({
availableLanguages
}) => {
const [isOpen, setIsOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
// Gérer les clics à l'extérieur
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
const handleEscapeKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('keydown', handleEscapeKey);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('keydown', handleEscapeKey);
};
}
}, [isOpen]);
const languageConfig: Record<Language, { name: string; flag: string; nativeName: string }> = {
fr: { name: 'Français', flag: '🇫🇷', nativeName: 'FR' },
@ -23,7 +53,7 @@ export const ModernLanguageSwitcher: React.FC<ModernLanguageSwitcherProps> = ({
const currentConfig = languageConfig[currentLanguage];
return (
<div className="relative">
<div className="relative" ref={containerRef}>
{/* Trigger Button */}
<button
onClick={() => setIsOpen(!isOpen)}
@ -31,7 +61,7 @@ export const ModernLanguageSwitcher: React.FC<ModernLanguageSwitcherProps> = ({
'flex items-center space-x-2 px-3 py-2 rounded-lg transition-all duration-200',
'bg-white/10 hover:bg-white/20 border border-white/20 hover:border-white/30',
'text-white text-sm font-medium',
'focus:outline-none focus:ring-2 focus:ring-banquise-blue-light/50',
'focus:outline-none focus:ring-2 focus:ring-blue-400/50',
'group'
)}
aria-expanded={isOpen}
@ -56,20 +86,12 @@ export const ModernLanguageSwitcher: React.FC<ModernLanguageSwitcherProps> = ({
{/* Dropdown Menu */}
{isOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-10"
onClick={() => setIsOpen(false)}
/>
{/* Menu */}
<div className={cn(
'absolute right-0 top-full mt-2 z-20',
'bg-white/95 backdrop-blur-xl rounded-xl shadow-2xl border border-white/20',
'min-w-[140px] py-2',
'animate-slideUp'
)}>
<div className={cn(
'absolute right-0 top-full mt-2 z-20',
'bg-white/95 backdrop-blur-xl rounded-xl shadow-2xl border border-white/20',
'min-w-[140px] py-2',
'animate-slideUp'
)}>
{availableLanguages.map((lang) => {
const config = languageConfig[lang];
const isSelected = lang === currentLanguage;
@ -83,11 +105,11 @@ export const ModernLanguageSwitcher: React.FC<ModernLanguageSwitcherProps> = ({
}}
className={cn(
'w-full flex items-center space-x-3 px-4 py-2.5 text-sm transition-all duration-200',
'hover:bg-banquise-blue/10 focus:bg-banquise-blue/10',
'hover:bg-blue-600/10 focus:bg-blue-600/10',
'focus:outline-none',
isSelected
? 'text-banquise-blue-dark font-semibold bg-banquise-blue/10'
: 'text-gray-700 hover:text-banquise-blue-dark'
? 'text-blue-800 font-semibold bg-blue-600/10'
: 'text-gray-700 hover:text-blue-800'
)}
role="option"
aria-selected={isSelected}
@ -95,7 +117,7 @@ export const ModernLanguageSwitcher: React.FC<ModernLanguageSwitcherProps> = ({
<span className="text-lg">{config.flag}</span>
<span className="flex-1 text-left">{config.name}</span>
{isSelected && (
<svg className="w-4 h-4 text-banquise-blue" fill="currentColor" viewBox="0 0 20 20">
<svg className="w-4 h-4 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
)}
@ -103,7 +125,6 @@ export const ModernLanguageSwitcher: React.FC<ModernLanguageSwitcherProps> = ({
);
})}
</div>
</>
)}
</div>
);

View File

@ -0,0 +1,88 @@
import React from 'react';
import { useOceanDepthEffect } from '@/lib/hooks/useOceanDepthEffect';
export const OceanBackground: React.FC = () => {
const scrollDepth = useOceanDepthEffect();
return (
<div className="fixed inset-0 pointer-events-none">
{/* Couche de base : dégradé océanique naturel de surface vers profondeur */}
<div className="absolute inset-0 bg-gradient-to-b from-banquise-blue-lightest via-banquise-blue-light via-banquise-blue to-banquise-blue-dark"></div>
{/* Couche d'assombrissement progressif basé sur le scroll */}
<div
className="absolute inset-0 bg-gradient-to-b from-transparent via-banquise-blue-dark/40 to-slate-900/80 transition-opacity duration-700 ease-out"
style={{
opacity: scrollDepth * 0.8,
transform: `translateY(${scrollDepth * 20}%)`
}}
/>
{/* Couche de profondeur extrême pour les abysses */}
<div
className="absolute inset-0 bg-gradient-to-b from-transparent via-slate-800/60 to-slate-950 transition-opacity duration-1000 ease-out"
style={{
opacity: Math.max(0, (scrollDepth - 0.4) * 1.5),
transform: `translateY(${scrollDepth * 30}%)`
}}
/>
{/* Rayons de lumière naturels qui s'estompent avec la profondeur */}
<div
className="absolute inset-0 transition-opacity duration-1000"
style={{ opacity: Math.max(0.2, 1 - scrollDepth * 1.2) }}
>
<div className="absolute top-0 left-0 w-full h-1/3 bg-gradient-to-br from-banquise-blue-lightest/15 via-transparent to-transparent" />
<div className="absolute top-0 right-0 w-full h-1/4 bg-gradient-to-bl from-banquise-blue-lightest/10 via-transparent to-transparent" />
<div className="absolute top-0 left-1/3 w-1 h-full bg-gradient-to-b from-banquise-blue-lightest/25 via-banquise-blue-lightest/5 to-transparent animate-ocean-shimmer" />
<div className="absolute top-0 left-2/3 w-1 h-full bg-gradient-to-b from-banquise-blue-lightest/20 via-banquise-blue-lightest/3 to-transparent animate-ocean-shimmer delay-1000" />
</div>
{/* Bulles marines qui deviennent plus rares en profondeur */}
<div className="absolute inset-0">
{/* Bulles de surface - nombreuses et actives */}
<div
className="absolute transition-opacity duration-700"
style={{ opacity: Math.max(0.3, 1 - scrollDepth * 0.8) }}
>
<div className="absolute top-[10%] left-[15%] w-3 h-3 bg-banquise-blue-lightest/30 rounded-full animate-bubble-float" />
<div className="absolute top-[20%] left-[75%] w-2 h-2 bg-banquise-blue-lightest/25 rounded-full animate-bubble-float-fast delay-300" />
<div className="absolute top-[25%] left-[40%] w-4 h-4 bg-banquise-blue-lightest/20 rounded-full animate-bubble-float-slow delay-600" />
<div className="absolute top-[35%] left-[80%] w-2 h-2 bg-banquise-blue-lightest/35 rounded-full animate-bubble-float delay-900" />
</div>
{/* Bulles de moyenne profondeur - moins nombreuses */}
<div
className="absolute transition-opacity duration-700"
style={{ opacity: Math.max(0.1, 0.6 - scrollDepth * 0.8) }}
>
<div className="absolute top-[50%] left-[25%] w-2 h-2 bg-banquise-blue-light/20 rounded-full animate-bubble-float-slow delay-1200" />
<div className="absolute top-[60%] left-[70%] w-1.5 h-1.5 bg-banquise-blue-light/15 rounded-full animate-bubble-float delay-1500" />
<div className="absolute top-[65%] left-[45%] w-3 h-3 bg-banquise-blue-light/10 rounded-full animate-bubble-float-fast delay-1800" />
</div>
{/* Bulles des profondeurs - très rares et subtiles */}
<div
className="absolute transition-opacity duration-1000"
style={{ opacity: Math.max(0, 0.3 - scrollDepth * 0.6) }}
>
<div className="absolute top-[80%] left-[20%] w-1 h-1 bg-banquise-blue/10 rounded-full animate-bubble-float-slow delay-2100" />
<div className="absolute top-[85%] left-[60%] w-1.5 h-1.5 bg-banquise-blue/8 rounded-full animate-bubble-float delay-2400" />
<div className="absolute top-[90%] left-[35%] w-1 h-1 bg-banquise-blue/5 rounded-full animate-bubble-float-fast delay-2700" />
</div>
</div>
{/* Effet de caustics (reflets de lumière sous l'eau) qui disparaît en profondeur */}
<div
className="absolute inset-0 transition-opacity duration-1000"
style={{ opacity: Math.max(0, 0.4 - scrollDepth * 0.7) }}
>
<div className="absolute top-0 left-0 w-full h-full opacity-20">
<div className="absolute top-[10%] left-[20%] w-32 h-1 bg-gradient-to-r from-transparent via-banquise-blue-lightest/40 to-transparent transform rotate-12 animate-ocean-shimmer" />
<div className="absolute top-[30%] left-[60%] w-24 h-1 bg-gradient-to-r from-transparent via-banquise-blue-lightest/30 to-transparent transform -rotate-6 animate-ocean-shimmer delay-2000" />
<div className="absolute top-[50%] left-[10%] w-40 h-1 bg-gradient-to-r from-transparent via-banquise-blue-lightest/20 to-transparent transform rotate-3 animate-ocean-shimmer delay-4000" />
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,149 @@
import React, { useEffect } from 'react';
import Image from 'next/image';
import { URLS } from '@/lib/config/constants';
import { FeatureBadge, FeatureItem, SectionTitle } from './PopupComponents';
import { cn, commonClasses } from '@/lib/utils';
import type { Service } from '@/types/service';
import type { Translation } from '@/types/i18n';
import { ClipboardList, Zap, Check, Lock, Rocket } from 'lucide-react';
interface PopupProps {
service: Service;
onClose: () => void;
translations: Translation['common'];
}
export const Popup: React.FC<PopupProps> = ({ service, onClose, translations }) => {
// Empêcher le scroll du body quand la popup est ouverte
useEffect(() => {
const originalStyle = document.body.style.overflow;
document.body.style.overflow = 'hidden';
return () => {
document.body.style.overflow = originalStyle || 'unset';
};
}, []);
return (
<div className="fixed inset-0 bg-black/60 flex justify-center items-center z-50 p-4 backdrop-blur-md animate-fadeIn">
<div className="bg-white text-gray-800 rounded-3xl max-w-4xl w-full max-h-[90vh] shadow-2xl relative animate-slideUp border border-gray-200 overflow-hidden">
{/* Bouton de fermeture fixe au-dessus du contenu */}
<div className="absolute top-4 right-4 z-50">
<button
onClick={onClose}
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}
>
×
</button>
</div>
{/* Contenu avec scroll vertical uniquement */}
<div className="overflow-y-auto overflow-x-hidden max-h-[90vh] popup-content">
{/* Header */}
<div className="relative bg-gradient-to-r from-blue-600 to-blue-500 p-6 sm:p-8 text-white pr-16 sm:pr-20">
<div className="flex flex-col lg:flex-row items-center lg:items-start mb-4">
<div className="w-16 h-16 sm:w-20 sm:h-20 lg:w-24 lg:h-24 bg-white/20 rounded-3xl flex items-center justify-center mb-4 lg:mb-0 lg:mr-8 backdrop-blur-sm">
{service.iconType === 'image' ? (
<Image
src={service.image as string}
alt={service.name}
width={48}
height={48}
className="w-12 h-12 sm:w-14 sm:h-14 lg:w-16 lg:h-16 object-contain"
/>
) : service.lucideIcon ? (
<service.lucideIcon className="w-12 h-12 sm:w-14 sm:h-14 lg:w-16 lg:h-16 text-white" strokeWidth={1.5} />
) : (
<span className="text-3xl sm:text-4xl lg:text-5xl">{service.icon}</span>
)}
</div>
<div className="text-center lg:text-left flex-1">
<h2 className="font-heading text-2xl sm:text-3xl lg:text-4xl mt-0 mb-3 lg:mb-4 leading-tight font-bold text-white">
{service.name}
</h2>
<div className="text-white/90 text-base sm:text-lg lg:text-xl font-medium">
Service d&apos;hébergement professionnel
</div>
<div className="mt-4 lg:mt-6 flex flex-wrap gap-2 justify-center lg:justify-start">
<span className="bg-white/20 text-white px-3 py-1 rounded-full text-sm font-medium backdrop-blur-sm">Haute disponibilité</span>
<span className="bg-white/20 text-white px-3 py-1 rounded-full text-sm font-medium backdrop-blur-sm">Open Source</span>
<span className="bg-white/20 text-white px-3 py-1 rounded-full text-sm font-medium backdrop-blur-sm">Communautaire</span>
</div>
</div>
</div>
</div>
{/* Content - Forcer le fond blanc */}
<div className="p-6 sm:p-8 bg-white">
{/* Description */}
<SectionTitle icon={ClipboardList} title="Description détaillée" />
<div className="bg-gradient-to-br from-blue-50 to-blue-100/50 rounded-2xl p-4 lg:p-6 border border-blue-200 mb-8">
<p className="text-gray-700 leading-relaxed text-base sm:text-lg lg:text-xl mb-4">
{service.description}
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-6">
<FeatureBadge
icon={Check}
title="99.9% Uptime"
subtitle="Disponibilité garantie"
/>
<FeatureBadge
icon={Lock}
title="Sécurisé"
subtitle="SSL & Backups"
/>
</div>
</div>
{/* Fonctionnalités */}
<SectionTitle icon={Zap} title={translations.discoverFeatures} />
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
{service.features.map((feature, index) => (
<FeatureItem key={index} feature={feature} index={index} />
))}
</div>
{/* Call to action */}
<div className="pt-6 lg:pt-8 border-t border-gray-200">
<a
href={service.url}
target="_blank"
rel="noopener noreferrer"
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.hoverScale,
'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500',
'text-base lg:text-lg'
)}
>
<Rocket className="w-6 h-6 lg:w-7 lg:h-7 mr-3" strokeWidth={2} />
<span>Accéder à {service.name}</span>
</a>
<p className="text-center text-sm text-gray-500 mt-4">
Besoin d&apos;aide ? Rejoignez notre <a href={URLS.social.discord} className={cn('text-blue-600 hover:text-blue-700 font-medium', commonClasses.transition)}>Discord</a> pour obtenir du support
</p>
</div>
</div>
</div>
{/* Decorative elements */}
<div className="absolute top-0 right-0 w-16 h-16 sm:w-24 sm:h-24 lg:w-32 lg:h-32 bg-blue-100/30 rounded-full -translate-y-8 translate-x-8 sm:-translate-y-12 sm:translate-x-12 lg:-translate-y-16 lg:translate-x-16 hidden sm:block pointer-events-none"></div>
<div className="absolute bottom-0 left-0 w-12 h-12 sm:w-16 sm:h-16 lg:w-24 lg:h-24 bg-blue-50 rounded-full translate-y-6 -translate-x-6 sm:translate-y-8 sm:-translate-x-8 lg:translate-y-12 lg:-translate-x-12 hidden sm:block pointer-events-none"></div>
</div>
</div>
);
};

View File

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

View File

@ -0,0 +1,29 @@
import React from 'react';
import { useScrollEffects } from '@/lib/hooks/useScrollEffects';
import { useTranslation } from '@/lib/hooks/useTranslation';
import { ArrowUp } from 'lucide-react';
export const ScrollToTopButton: React.FC = () => {
const { isVisible, scrollToTop } = useScrollEffects();
const { t } = useTranslation();
return (
<button
onClick={scrollToTop}
className={`fixed bottom-6 right-6 z-50 w-12 h-12 sm:w-14 sm:h-14 bg-gradient-to-r from-blue-600 to-blue-500 text-white rounded-full shadow-lg hover:shadow-xl transition-all duration-300 flex items-center justify-center group border border-blue-400/30 backdrop-blur-sm ${
isVisible
? 'opacity-100 translate-y-0 scale-100'
: 'opacity-0 translate-y-4 scale-95 pointer-events-none'
}`}
aria-label={t.common.backToTop}
>
<ArrowUp
className="w-5 h-5 sm:w-6 sm:h-6 transition-transform duration-300 group-hover:-translate-y-0.5"
strokeWidth={2.5}
/>
{/* Effet de lueur au hover */}
<div className="absolute inset-0 bg-gradient-to-r from-blue-500 to-blue-600 rounded-full opacity-0 group-hover:opacity-75 transition-opacity duration-300 blur-sm"></div>
</button>
);
};

View File

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

View File

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

View File

@ -1,28 +0,0 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

View File

@ -1,19 +0,0 @@
<!doctype html>
<html lang="fr"> <!-- Changement de "en" à "fr" pour refléter la langue du contenu -->
<head>
<meta charset="UTF-8" />
<!-- Remplacement du favicon par le logo de La Banquise -->
<link rel="icon" type="image/png" href="/src/assets/banquise.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="description" content="Services d'hébergement La Banquise - Accédez à notre Wiki, Gitea et Panel de jeux" />
<title>La Banquise - Services d'hébergement</title>
<!-- Ajout des polices Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Dela+Gothic+One&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -1,17 +1,15 @@
import type { Translation } from '../../types/i18n';
import { URLS } from '../../config/constants';
import logoGitea from '/src/assets/Gitea_Logo.png';
import logoWiki from '/src/assets/wikijs.png';
import logoPelican from '/src/assets/pelican.png';
import logoOpencloud from '/src/assets/opencloud_logo_white.png';
import type { Translation } from '@/types/i18n';
import { URLS } from '@/lib/config/constants';
import { BookOpen, GitBranch, Gamepad2, KeyRound, Building, Mail } from 'lucide-react';
export const en: Translation = {
services: [
{
name: "Wiki",
url: URLS.services.wiki,
image: logoWiki,
image: "/assets/wikijs.png",
icon: "📚",
iconType: "image",
description: "Collaborative technical documentation and knowledge sharing platform. Create, edit and organize your guides, tutorials and documentation as a team with integrated versioning system.",
features: [
"Advanced markdown editor with real-time preview",
@ -27,8 +25,9 @@ export const en: Translation = {
{
name: "Gitea",
url: URLS.services.gitea,
image: logoGitea,
image: "/assets/Gitea_Logo.png",
icon: "🔧",
iconType: "image",
description: "Lightweight and performant self-hosted Git service for your development projects. Open-source alternative to GitHub with all essential features for managing your repositories.",
features: [
"Unlimited public and private Git repositories",
@ -44,8 +43,9 @@ export const en: Translation = {
{
name: "Pelican",
url: URLS.services.pelican,
image: logoPelican,
image: "/assets/pelican.png",
icon: "🐧",
iconType: "image",
description: "Game server management with dedicated servers (Minecraft, CS2, Palworld, and many others)",
features: [
"One-click deployment with pre-configured templates",
@ -59,8 +59,10 @@ export const en: Translation = {
{
name: "Password Change",
url: URLS.services.ssp,
image: "/path/to/ssp-image.jpg",
image: "/assets/banquise.png",
icon: "🔐",
iconType: "lucide",
lucideIcon: KeyRound,
description: "Secure interface for autonomous password management. Easily change your credentials safely.",
features: [
"Secure interface to change your password",
@ -72,8 +74,9 @@ export const en: Translation = {
{
name: "OpenCloud",
url: URLS.services.opencloud,
image: logoOpencloud,
image: "/assets/opencloud_logo_white.png",
icon: "☁️",
iconType: "image",
description: "Open-source collaborative cloud platform for file storage, sharing and synchronization. Free alternative to Google Drive with full control over your data.",
features: [
"Secure and encrypted cloud storage",
@ -89,8 +92,10 @@ export const en: Translation = {
{
name: "Intranet",
url: URLS.services.intra,
image: "/path/to/intra-image.jpg",
image: "/assets/banquise.png",
icon: "🏢",
iconType: "lucide",
lucideIcon: Building,
description: "Secure private space for the association to centralize internal resources, communications and collaboration tools between members.",
features: [
"Personalized dashboard for each member",
@ -106,8 +111,10 @@ export const en: Translation = {
{
name: "Gaming Panel",
url: URLS.services.panel,
image: "/path/to/panel-image.jpg",
image: "/assets/banquise.png",
icon: "🎮",
iconType: "lucide",
lucideIcon: Gamepad2,
description: "Centralized management interface for all your game servers. Easily deploy, configure and monitor your Minecraft, CS2, Garry's Mod and many other servers.",
features: [
"Support for 20+ popular games (Minecraft, CS2, GMod...)",
@ -122,8 +129,10 @@ export const en: Translation = {
{
name: "Mails",
url: URLS.services.mails,
image: "/path/to/mails-image.jpg",
image: "/assets/banquise.png",
icon: "📧",
iconType: "lucide",
lucideIcon: Mail,
description: "Professional email service with modern web interface. Benefit from a personalized @la-banquise.fr email address with all advanced features.",
features: [
"Personalized @la-banquise.fr email addresses",
@ -151,6 +160,117 @@ export const en: Translation = {
common: {
discoverFeatures: "Discover all features",
close: "Close",
loading: "Loading..."
loading: "Loading...",
learnMore: "Learn more",
backToHome: "Back to home",
discoverOffer: "Discover our offer",
learnMoreAboutUs: "Learn more about us",
sendEmail: "Send us an email",
login: "Sign in",
joinCommunity: "Join community",
backToTop: "Back to top"
},
user: {
profile: "Profile",
logout: "Sign out",
groups: "Groups",
userMenu: "User menu",
connecting: "Connecting...",
authError: "Authentication error"
},
sections: {
ourServices: "Our Services"
},
about: {
title: "About La Banquise",
subtitle: "A passionate community that provides hosting services and collaborative tools for developers and gamers.",
faqTitle: "Frequently Asked Questions",
mission: {
title: "Our Mission",
description1: "Train students in deploying and managing infrastructure, and mastering enterprise-grade technologies. This allows us to provide a stable and accessible platform to host your projects, share your knowledge and play together!",
description2: "We believe in the power of collaboration and provide modern tools to facilitate teamwork.",
tags: {
collaboration: "Collaboration",
innovation: "Innovation",
accessibility: "Accessibility"
}
},
services: {
title: "Our Services",
wiki: {
title: "Wiki",
description: "Collaborative documentation and detailed guides"
},
gitea: {
title: "Gitea",
description: "Self-hosted Git version control"
},
panel: {
title: "Gaming Panel",
description: "Management interface for game servers"
},
pelican: {
title: "Pelican",
description: "Static site generator"
},
intranet: {
title: "Intranet",
description: "Private association space"
},
mails: {
title: "Webmail",
description: "Email messaging service"
},
opencloud: {
title: "OpenCloud",
description: "Collaborative cloud platform for all your needs"
},
note: "All our services are carefully maintained and regularly updated to ensure an optimal experience."
},
community: {
title: "Join the association",
description: "Join our Discord server to join the association, chat with us, get help and stay informed about the latest news!",
howToJoin: "How to join the association?",
steps: {
step1: "Create a banquise ticket",
step2: "Provide your EPITA login or explain your situation",
step3: "A moderator will validate your request and give you access to the association's discord channels!"
},
joinDiscord: "Join Discord"
}
},
footer: {
description: "A passionate community that provides hosting services and collaborative tools for developers and gamers.",
ourServices: "Our Services",
community: "Community",
joinAssociation: "Join the association",
joinDescription: "Connect on Discord and create a ticket to join the Banquise community.",
joinNow: "Join now",
gamingPanel: "Gaming Panel",
madeWith: "Made with",
by: "by Banquise",
copyright: "Community hosting for developers and gamers."
},
infrastructure: {
title: "Our Infrastructure",
subtitle: "25+ servers to meet your needs",
features: {
performance: {
title: "High-performance servers",
description: "Optimized infrastructure to ensure high performance and maximum availability of your applications"
},
storage: {
title: "Secure storage",
description: "Distributed storage solutions with redundancy to guarantee the integrity and durability of your data"
},
network: {
title: "Optimized network",
description: "High-availability network architecture with low latency for your critical applications"
},
security: {
title: "Enhanced security",
description: "Threat protection with modern security systems and regular updates"
}
}
}
};

View File

@ -1,17 +1,15 @@
import type { Translation } from '../../types/i18n';
import { URLS } from '../../config/constants';
import logoGitea from '/src/assets/Gitea_Logo.png';
import logoWiki from '/src/assets/wikijs.png';
import logoPelican from '/src/assets/pelican.png';
import logoOpencloud from '/src/assets/opencloud_logo_white.png';
import type { Translation } from '@/types/i18n';
import { URLS } from '@/lib/config/constants';
import { BookOpen, GitBranch, Gamepad2, KeyRound, Building, Mail } from 'lucide-react';
export const fr: Translation = {
services: [
{
name: "Wiki",
url: URLS.services.wiki,
image: logoWiki,
image: "/assets/wikijs.png",
icon: "📚",
iconType: "image",
description: "Plateforme collaborative de documentation technique et de partage de connaissances. Créez, modifiez et organisez vos guides, tutoriels et documentations en équipe avec un système de versioning intégré.",
features: [
"Éditeur markdown avancé avec prévisualisation en temps réel",
@ -27,8 +25,9 @@ export const fr: Translation = {
{
name: "Gitea",
url: URLS.services.gitea,
image: logoGitea,
image: "/assets/Gitea_Logo.png",
icon: "🔧",
iconType: "image",
description: "Service Git auto-hébergé lightweight et performant pour vos projets de développement. Alternative open-source à GitHub avec toutes les fonctionnalités essentielles pour gérer vos repositories.",
features: [
"Repositories Git illimités publics et privés",
@ -44,8 +43,9 @@ export const fr: Translation = {
{
name: "Pelican",
url: URLS.services.pelican,
image: logoPelican,
image: "/assets/pelican.png",
icon: "🐧",
iconType: "image",
description: "Gestion de serveurs de jeux avec serveurs dédiés (Minecraft, CS2, Palworld, et bien d'autres)",
features: [
"Déploiement en un clic avec templates préconfigurés",
@ -59,8 +59,10 @@ export const fr: Translation = {
{
name: "Changement de mot de passe",
url: URLS.services.ssp,
image: "/path/to/ssp-image.jpg",
image: "/assets/banquise.png",
icon: "🔐",
iconType: "lucide",
lucideIcon: KeyRound,
description: "Interface sécurisée pour la gestion autonome de vos mots de passe. Changez facilement vos identifiants en toute sécurité.",
features: [
"Interface sécurisée pour changer votre mot de passe",
@ -72,8 +74,9 @@ export const fr: Translation = {
{
name: "OpenCloud",
url: URLS.services.opencloud,
image: logoOpencloud,
image: "/assets/opencloud_logo_white.png",
icon: "☁️",
iconType: "image",
description: "Plateforme cloud collaborative open-source pour le stockage, le partage et la synchronisation de fichiers. Alternative libre à Google Drive avec contrôle total sur vos données.",
features: [
"Stockage cloud sécurisé et chiffré",
@ -89,8 +92,10 @@ export const fr: Translation = {
{
name: "Intranet",
url: URLS.services.intra,
image: "/path/to/intra-image.jpg",
image: "/assets/banquise.png",
icon: "🏢",
iconType: "lucide",
lucideIcon: Building,
description: "Espace privé sécurisé de l'association pour centraliser les ressources internes, communications et outils de collaboration entre membres.",
features: [
"Tableau de bord personnalisé pour chaque membre",
@ -106,8 +111,10 @@ export const fr: Translation = {
{
name: "Panel Gaming",
url: URLS.services.panel,
image: "/path/to/panel-image.jpg",
image: "/assets/banquise.png",
icon: "🎮",
iconType: "lucide",
lucideIcon: Gamepad2,
description: "Interface de gestion centralisée pour tous vos serveurs de jeux. Déployez, configurez et surveillez facilement vos serveurs Minecraft, CS2, Garry's Mod et bien d'autres.",
features: [
"Support de 20+ jeux populaires (Minecraft, CS2, GMod...)",
@ -122,8 +129,10 @@ export const fr: Translation = {
{
name: "Mails",
url: URLS.services.mails,
image: "/path/to/mails-image.jpg",
image: "/assets/banquise.png",
icon: "📧",
iconType: "lucide",
lucideIcon: Mail,
description: "Service de messagerie électronique professionnel avec interface web moderne. Bénéficiez d'une adresse email personnalisée @la-banquise.fr avec toutes les fonctionnalités avancées.",
features: [
"Adresses email personnalisées @la-banquise.fr",
@ -151,6 +160,117 @@ export const fr: Translation = {
common: {
discoverFeatures: "Découvrir toutes les fonctionnalités",
close: "Fermer",
loading: "Chargement..."
loading: "Chargement...",
learnMore: "En savoir plus",
backToHome: "Retour à l'accueil",
discoverOffer: "Découvrir notre offre",
learnMoreAboutUs: "En savoir plus sur nous",
sendEmail: "Nous envoyer un email",
login: "Se connecter",
joinCommunity: "Rejoindre la communauté",
backToTop: "Retour en haut de page"
},
user: {
profile: "Profil",
logout: "Se déconnecter",
groups: "Groupes",
userMenu: "Menu utilisateur",
connecting: "Connexion en cours...",
authError: "Erreur d'authentification"
},
sections: {
ourServices: "Nos Services"
},
about: {
title: "À Propos de La Banquise",
subtitle: "Une communauté passionnée qui propose des services d'hébergement et des outils collaboratifs pour les développeurs et les gamers.",
faqTitle: "Questions Fréquentes",
mission: {
title: "Notre Mission",
description1: "Former les étudiants au déploiement et à la gestion d'une infrastructure, et de maîtriser des technologies entreprise grade. Cela permet de fournir une plateforme stable et accessible pour héberger vos projets, partager vos connaissances et jouer ensemble !",
description2: "Nous croyons en la puissance de la collaboration et mettons à disposition des outils modernes pour faciliter le travail en équipe.",
tags: {
collaboration: "Collaboration",
innovation: "Innovation",
accessibility: "Accessibilité"
}
},
services: {
title: "Nos Services",
wiki: {
title: "Wiki",
description: "Documentation collaborative et guides détaillés"
},
gitea: {
title: "Gitea",
description: "Gestion de versions Git auto-hébergée"
},
panel: {
title: "Panel de Jeux",
description: "Interface de gestion pour serveurs de jeux"
},
pelican: {
title: "Pelican",
description: "Générateur de sites statiques"
},
intranet: {
title: "Intranet",
description: "Espace privé de l'association"
},
mails: {
title: "Webmail",
description: "Service de messagerie électronique"
},
opencloud: {
title: "OpenCloud",
description: "Plateforme cloud collaborative pour tous vos besoins"
},
note: "Tous nos services sont maintenus avec soin et régulièrement mis à jour pour garantir une expérience optimale."
},
community: {
title: "Rejoindre l'association",
description: "Rejoignez notre serveur Discord pour rejoindre l'asso, échanger avec nous, obtenir de l'aide et rester informé des dernières nouveautés !",
howToJoin: "Comment rejoindre l'asso ?",
steps: {
step1: "Créez un ticket banquise",
step2: "Donnez votre login EPITA ou expliquez votre situation",
step3: "Un modérateur validera votre demande et vous donnera accès aux salons discord de l'asso !"
},
joinDiscord: "Rejoindre Discord"
}
},
footer: {
description: "Une communauté passionnée qui propose des services d'hébergement et des outils collaboratifs pour les développeurs et les gamers.",
ourServices: "Nos Services",
community: "Communauté",
joinAssociation: "Rejoindre l'asso",
joinDescription: "Connectez-vous sur Discord et créez un ticket pour rejoindre la communauté Banquise.",
joinNow: "Rejoindre maintenant",
gamingPanel: "Panel de Jeux",
madeWith: "Fait avec",
by: "par Banquise",
copyright: "Hébergement communautaire pour développeurs et gamers."
},
infrastructure: {
title: "Notre Infrastructure",
subtitle: "25+ serveurs pour répondre à vos besoins",
features: {
performance: {
title: "Serveurs performants",
description: "Infrastructure optimisée pour assurer des performances élevées et une disponibilité maximale de vos applications"
},
storage: {
title: "Stockage sécurisé",
description: "Solutions de stockage distribuées avec redondance pour garantir l'intégrité et la durabilité de vos données"
},
network: {
title: "Réseau optimisé",
description: "Architecture réseau à haute disponibilité avec une faible latence pour vos applications critiques"
},
security: {
title: "Sécurité renforcée",
description: "Protection contre les menaces avec systèmes de sécurité modernes et mises à jour régulières"
}
}
}
};

View File

@ -1,6 +1,6 @@
import { fr } from './fr';
import { en } from './en';
import type { Language, Translation } from '../../types/i18n';
import type { Language, Translation } from '@/types/i18n';
export const translations: Record<Language, Translation> = {
fr,

View File

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

View File

@ -0,0 +1,55 @@
import { useState, useEffect } from 'react';
import { authService, type AuthStatus } from '@/lib/services/auth';
export const useAuth = () => {
const [authStatus, setAuthStatus] = useState<AuthStatus>({
isAuthenticated: false,
});
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const checkAuth = async () => {
try {
setIsLoading(true);
setError(null);
const status = await authService.checkAuthStatus();
setAuthStatus(status);
} catch (err) {
setError('Erreur lors de la vérification de l\'authentification');
console.error('Auth check error:', err);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
checkAuth();
}, []);
const login = () => {
authService.login();
};
const logout = async () => {
try {
await authService.logout();
setAuthStatus({ isAuthenticated: false });
} catch (err) {
setError('Erreur lors de la déconnexion');
console.error('Logout error:', err);
}
};
const refreshAuth = () => {
checkAuth();
};
return {
...authStatus,
isLoading,
error,
login,
logout,
refreshAuth,
};
};

View File

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

View File

@ -0,0 +1,35 @@
import { useState, useEffect } from 'react';
export const useOceanDepthEffect = () => {
const [scrollDepth, setScrollDepth] = useState(0);
useEffect(() => {
const handleScroll = () => {
const scrollPosition = window.scrollY;
const documentHeight = document.documentElement.scrollHeight - window.innerHeight;
if (documentHeight <= 0) {
setScrollDepth(0);
return;
}
// Calcul de la profondeur avec courbe d'accélération naturelle
const rawPercentage = Math.min(scrollPosition / documentHeight, 1);
// Courbe d'easing pour un effet plus naturel de descente océanique
// Plus on descend, plus l'assombrissement s'accélère (comme dans l'océan réel)
const easedDepth = rawPercentage < 0.5
? 2 * rawPercentage * rawPercentage
: 1 - Math.pow(-2 * rawPercentage + 2, 3) / 2;
setScrollDepth(Math.min(easedDepth, 1));
};
window.addEventListener('scroll', handleScroll, { passive: true });
handleScroll(); // Initial call
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return scrollDepth;
};

View File

@ -1,5 +1,5 @@
import { useState, useCallback } from 'react';
import type { Service } from '../types/service';
import type { Service } from '@/types/service';
/**
* Hook personnalisé pour gérer l'état des modales de services

View File

@ -0,0 +1,50 @@
import { useState, useEffect, useMemo } from 'react';
import type { Language, Translation } from '@/types/i18n';
import { translations, defaultLanguage } from '@/lib/data/translations';
export const useTranslation = () => {
// Initialize on server with default language to avoid using localStorage during SSR
const [currentLanguage, setCurrentLanguage] = useState<Language>(defaultLanguage);
// On client, read saved language from localStorage
useEffect(() => {
try {
const saved = (typeof window !== 'undefined' && localStorage.getItem('language')) as Language | null;
if (saved && translations[saved]) {
setCurrentLanguage(saved);
}
} catch (e) {
// ignore (e.g., localStorage not available)
}
}, []);
// Persist language changes on client
useEffect(() => {
try {
if (typeof window !== 'undefined') {
localStorage.setItem('language', currentLanguage);
}
} catch (e) {
// ignore
}
}, [currentLanguage]);
// Memoize the translation object to prevent unnecessary re-renders
const t = useMemo<Translation>(() => translations[currentLanguage], [currentLanguage]);
// Memoize available languages array
const availableLanguages = useMemo(() => Object.keys(translations) as Language[], []);
const changeLanguage = (language: Language) => {
if (translations[language]) {
setCurrentLanguage(language);
}
};
return {
t,
currentLanguage,
changeLanguage,
availableLanguages,
};
};

View File

@ -0,0 +1,57 @@
import { URLS } from '@/lib/config/constants';
export interface AutheliaUser {
name: string;
email: string;
groups: string[];
avatar?: string;
}
export interface AuthStatus {
isAuthenticated: boolean;
user?: AutheliaUser;
}
class AuthService {
// Keep the constant in case other code expects it, but we no longer call it
private authBaseUrl = URLS?.services?.auth ?? '';
/**
* Authelia integration intentionally disabled.
* Return unauthenticated status to remove external links while preserving API.
*/
async checkAuthStatus(): Promise<AuthStatus> {
return { isAuthenticated: false };
}
/**
* No user info available when Authelia is disabled.
*/
async getUserInfo(): Promise<AutheliaUser | undefined> {
return undefined;
}
private generateAvatarUrl(_identifier: string): string {
return '';
}
private simpleHash(_str: string): string {
return '';
}
/**
* Disabled: do nothing instead of redirecting to an external auth provider.
*/
login(): void {
return;
}
/**
* Disabled: no-op logout.
*/
async logout(): Promise<void> {
return;
}
}
export const authService = new AuthService();

View File

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

View File

@ -0,0 +1,79 @@
// 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-200 ease-in-out',
// Hover effects communs
hoverScale: 'hover:scale-105 active:scale-95',
cardHover: 'hover:shadow-xl hover:-translate-y-1',
// Boutons communs
buttonBase: 'inline-flex items-center justify-center font-semibold rounded-xl transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-400/50',
// Navigation
navLink: 'px-4 py-2 text-white/90 hover:text-white font-medium rounded-lg transition-colors duration-200 hover:bg-white/10',
} as const;

6
banquise-website/next-env.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

File diff suppressed because it is too large Load Diff

View File

@ -4,41 +4,29 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
"dev": "next dev",
"build": "next build",
"start": "next start -p $PORT",
"lint": "next lint"
},
"dependencies": {
"axios": "^1.6.5",
"clsx": "^2.1.0",
"framer-motion": "^10.18.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.12.0",
"react-router-dom": "^6.22.0",
"@tanstack/react-query": "^5.17.9",
"tailwind-merge": "^2.2.0",
"zustand": "^4.4.7"
"lucide-react": "^0.544.0",
"next": "^15.5.3",
"react": "^19.1.1",
"react-dom": "^19.1.1"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"@types/react": "^18.2.58",
"@types/react-dom": "^18.2.19",
"@vitejs/plugin-react": "^4.4.1",
"autoprefixer": "^10.4.16",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"eslint-plugin-tailwindcss": "^3.14.0",
"globals": "^16.0.0",
"postcss": "^8.4.33",
"typescript": "~5.8.3",
"typescript-eslint": "^8.30.1",
"vite": "^6.3.5",
"vite-plugin-compression": "^0.5.1",
"tailwindcss": "^3.4.1"
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/postcss": "^4.1.13",
"@tailwindcss/typography": "^0.5.16",
"@types/node": "24.3.3",
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9",
"autoprefixer": "^10.4.21",
"eslint": "^9.35.0",
"eslint-config-next": "^15.5.3",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.13",
"typescript": "~5.9.2"
}
}

3928
banquise-website/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
export default {
plugins: {
tailwindcss: {},
'@tailwindcss/postcss': {},
autoprefixer: {},
},
}

View File

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 115 KiB

View File

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 188 KiB

View File

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 121 KiB

View File

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

View File

@ -1 +0,0 @@
/* This file is no longer needed - all styles have been converted to Tailwind CSS */

View File

@ -1,147 +0,0 @@
import React from 'react';
import { ModernNavigation } from './components/layout/ModernNavigation';
import { HeroSection } from './components/sections/HeroSection';
import { ServicesSection } from './components/sections/ServicesSection';
import { TechFeaturesSection } from './components/sections/TechFeaturesSection';
import { AboutSection } from './components/sections/AboutSection';
import { Footer } from './components/layout/Footer';
import { Popup } from './components/ui/Popup';
import { ScrollToTopButton } from './components/ui/ScrollToTopButton';
import { ModernLanguageSwitcher } from './components/ui/ModernLanguageSwitcher';
import { useTranslation } from './hooks/useTranslation';
import { useServiceModal } from './hooks/useServiceModal';
import { useAccordion } from './hooks/useAccordion';
import { useOceanDepthEffect } from './hooks/useOceanDepthEffect';
const App: React.FC = () => {
const { t, currentLanguage, changeLanguage, availableLanguages } = useTranslation();
const { selectedService, openServiceModal, closeServiceModal } = useServiceModal();
const { openAccordion, toggleAccordion } = useAccordion();
const scrollDepth = useOceanDepthEffect();
return (
<div className="min-h-screen relative overflow-x-hidden">
{/* Arrière-plan océanique uniforme avec assombrissement progressif basé sur le scroll */}
<div className="fixed inset-0 pointer-events-none">
{/* Dégradé principal océanique - Surface (clair) vers abysses (très sombre) */}
<div className="absolute inset-0 bg-gradient-to-b from-banquise-blue-light via-banquise-blue via-banquise-blue-dark to-banquise-blue-dark"></div>
{/* Couche de profondeur progressive basée sur le scroll - Plus intense */}
<div
className="absolute inset-0 bg-gradient-to-b from-transparent via-banquise-blue-dark/60 to-banquise-blue-dark transition-opacity duration-500"
style={{ opacity: scrollDepth * 0.9 }}
></div>
{/* Effet de profondeur supplémentaire pour les moyennes profondeurs - Plus sombre */}
<div
className="absolute inset-0 bg-gradient-to-b from-transparent via-banquise-blue-dark/70 to-banquise-blue-dark transition-opacity duration-500"
style={{ opacity: Math.max(0, (scrollDepth - 0.2) * 1.5) }}
></div>
{/* Assombrissement pour les grandes profondeurs - Plus intense */}
<div
className="absolute inset-0 bg-gradient-to-b from-transparent via-banquise-blue-dark/80 to-slate-900 transition-opacity duration-500"
style={{ opacity: Math.max(0, (scrollDepth - 0.5) * 2) }}
></div>
{/* Assombrissement final pour les abysses - Très sombre */}
<div
className="absolute inset-0 bg-gradient-to-b from-banquise-blue-dark/50 to-slate-900 transition-opacity duration-500"
style={{ opacity: Math.max(0, (scrollDepth - 0.7) * 2.5) }}
></div>
{/* Rayons de lumière subtils qui percent l'eau depuis la surface */}
<div className="absolute inset-0 bg-gradient-to-br from-banquise-blue-lightest/4 via-transparent to-transparent"></div>
<div className="absolute inset-0 bg-gradient-to-bl from-transparent via-banquise-blue-lightest/2 to-transparent"></div>
{/* Effet de scintillement subtil avec animations océaniques */}
<div className="absolute inset-0">
<div className="absolute top-0 left-1/4 w-px h-full bg-gradient-to-b from-banquise-blue-lightest/20 via-banquise-blue-lightest/8 to-transparent animate-ocean-shimmer"></div>
<div className="absolute top-0 left-2/3 w-px h-full bg-gradient-to-b from-banquise-blue-lightest/15 via-banquise-blue-lightest/6 to-transparent animate-ocean-shimmer delay-1000"></div>
<div className="absolute top-0 left-3/4 w-px h-full bg-gradient-to-b from-banquise-blue-lightest/10 via-banquise-blue-lightest/4 to-transparent animate-ocean-shimmer delay-2000"></div>
</div>
{/* Particules flottantes (bulles) pour l'effet sous-marin avec animations variées */}
<div className="absolute inset-0">
{/* Bulles réparties sur toute la hauteur pour l'effet océanique continu */}
<div className="absolute top-1/6 left-1/5 w-2 h-2 bg-banquise-blue-lightest/20 rounded-full animate-bubble-float"></div>
<div className="absolute top-1/4 left-3/4 w-1 h-1 bg-banquise-blue-lightest/15 rounded-full animate-bubble-float-fast delay-500"></div>
<div className="absolute top-1/3 left-1/3 w-3 h-3 bg-banquise-blue-lightest/10 rounded-full animate-bubble-float-slow"></div>
<div className="absolute top-1/2 left-4/5 w-1.5 h-1.5 bg-banquise-blue-lightest/18 rounded-full animate-bubble-float delay-1000"></div>
<div className="absolute top-2/3 left-1/6 w-2.5 h-2.5 bg-banquise-blue-lightest/8 rounded-full animate-bubble-float-slow delay-1500"></div>
<div className="absolute top-3/4 left-2/3 w-1 h-1 bg-banquise-blue-lightest/22 rounded-full animate-bubble-float-fast delay-700"></div>
<div className="absolute top-5/6 left-1/2 w-1.5 h-1.5 bg-banquise-blue-lightest/12 rounded-full animate-bubble-float delay-300"></div>
<div className="absolute top-11/12 left-3/5 w-2 h-2 bg-banquise-blue-lightest/6 rounded-full animate-bubble-float-slow delay-2000"></div>
{/* Bulles supplémentaires pour un effet plus dense */}
<div className="absolute top-1/8 left-1/2 w-1.5 h-1.5 bg-banquise-blue-lightest/25 rounded-full animate-bubble-float-fast delay-800"></div>
<div className="absolute top-3/8 left-1/8 w-2 h-2 bg-banquise-blue-lightest/12 rounded-full animate-bubble-float delay-1200"></div>
<div className="absolute top-5/8 left-7/8 w-1 h-1 bg-banquise-blue-lightest/20 rounded-full animate-bubble-float-slow delay-600"></div>
<div className="absolute top-7/8 left-1/4 w-2.5 h-2.5 bg-banquise-blue-lightest/8 rounded-full animate-bubble-float-fast delay-1800"></div>
<div className="absolute top-1/7 left-5/6 w-1 h-1 bg-banquise-blue-lightest/28 rounded-full animate-bubble-float delay-400"></div>
<div className="absolute top-2/7 left-2/5 w-1.5 h-1.5 bg-banquise-blue-lightest/15 rounded-full animate-bubble-float-slow delay-900"></div>
<div className="absolute top-4/7 left-3/8 w-2 h-2 bg-banquise-blue-lightest/10 rounded-full animate-bubble-float-fast delay-1400"></div>
<div className="absolute top-6/7 left-4/5 w-1 h-1 bg-banquise-blue-lightest/18 rounded-full animate-bubble-float delay-200"></div>
{/* Bulles très petites pour densité */}
<div className="absolute top-1/10 left-3/10 w-0.5 h-0.5 bg-banquise-blue-lightest/30 rounded-full animate-bubble-float-fast delay-100"></div>
<div className="absolute top-3/10 left-7/10 w-0.5 h-0.5 bg-banquise-blue-lightest/25 rounded-full animate-bubble-float delay-1100"></div>
<div className="absolute top-7/10 left-1/10 w-0.5 h-0.5 bg-banquise-blue-lightest/20 rounded-full animate-bubble-float-slow delay-1700"></div>
<div className="absolute top-9/10 left-9/10 w-0.5 h-0.5 bg-banquise-blue-lightest/15 rounded-full animate-bubble-float-fast delay-2200"></div>
{/* Bulles moyennes pour variation */}
<div className="absolute top-1/5 left-4/7 w-3 h-3 bg-banquise-blue-lightest/5 rounded-full animate-bubble-float-slow delay-1600"></div>
<div className="absolute top-2/5 left-6/7 w-2.5 h-2.5 bg-banquise-blue-lightest/7 rounded-full animate-bubble-float delay-800"></div>
<div className="absolute top-4/5 left-2/7 w-3.5 h-3.5 bg-banquise-blue-lightest/4 rounded-full animate-bubble-float-slow delay-2400"></div>
</div>
</div>
{/* Contenu principal avec arrière-plan océanique uniforme */}
<div className="relative z-10 text-white">
{/* Navigation flottante */}
<ModernNavigation
translations={t.navigation}
languageSwitcher={
<ModernLanguageSwitcher
currentLanguage={currentLanguage}
onLanguageChange={changeLanguage}
availableLanguages={availableLanguages}
/>
}
/>
{/* Section Hero - Surface de l'océan */}
<HeroSection translations={t.hero} />
{/* Section Services */}
<ServicesSection
services={t.services}
onServiceClick={openServiceModal}
translations={t.common}
/>
{/* Section TechFeatures */}
<TechFeaturesSection />
{/* Section About */}
<AboutSection openAccordion={openAccordion} toggleAccordion={toggleAccordion} />
{/* Footer */}
<Footer />
</div>
{/* UI Components */}
<ScrollToTopButton />
{selectedService && (
<Popup
service={selectedService}
onClose={closeServiceModal}
translations={t.common}
/>
)}
</div>
);
};
export default App;

View File

@ -1,65 +0,0 @@
import React from 'react';
import { componentStyles, mergeClasses } from '../../styles/designSystem';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'discord' | 'auth' | 'secondary';
size?: 'sm' | 'md' | 'lg';
fullWidth?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
loading?: boolean;
children: React.ReactNode;
}
const sizeClasses = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 lg:px-6 py-2.5 lg:py-3 text-sm lg:text-base',
lg: 'px-6 py-3 text-base lg:text-lg',
};
const variantClasses = {
primary: 'bg-gradient-to-r from-banquise-blue to-banquise-blue-light hover:shadow-banquise-blue/25',
discord: 'bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500 hover:shadow-indigo-500/25',
auth: 'bg-gradient-to-r from-banquise-blue-light to-banquise-blue hover:shadow-banquise-blue-light/25',
secondary: 'bg-white/10 hover:bg-white/20 border border-white/20',
};
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
fullWidth = false,
leftIcon,
rightIcon,
loading = false,
children,
className = '',
disabled,
...props
}) => {
const baseClasses = mergeClasses(
componentStyles.button.base,
sizeClasses[size],
variantClasses[variant],
fullWidth ? 'w-full' : '',
(disabled || loading) ? 'opacity-50 cursor-not-allowed' : '',
className
);
return (
<button
className={baseClasses}
disabled={disabled || loading}
{...props}
>
{loading && (
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" 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>}
{children}
{rightIcon && !loading && <span className="ml-2">{rightIcon}</span>}
</button>
);
};

View File

@ -1,40 +0,0 @@
import React from 'react';
import { componentStyles, mergeClasses } from '../../styles/designSystem';
interface CardProps {
variant?: 'default' | 'interactive' | 'service';
className?: string;
children: React.ReactNode;
onClick?: () => void;
}
const variantClasses = {
default: 'bg-gradient-to-br from-banquise-blue-dark/10 to-banquise-blue-dark/5',
interactive: 'bg-gradient-to-br from-banquise-blue-dark/10 to-banquise-blue-dark/5 cursor-pointer hover:-translate-y-4 hover:shadow-2xl hover:border-banquise-blue-lightest/50 hover:from-banquise-blue-dark/15 hover:to-banquise-blue-dark/8 active:scale-95',
service: 'bg-gradient-to-br from-banquise-blue-dark/10 to-banquise-blue-dark/5 cursor-pointer hover:-translate-y-4 hover:shadow-2xl hover:border-banquise-blue-lightest/50 hover:from-banquise-blue-dark/15 hover:to-banquise-blue-dark/8 active:scale-95',
};
export const Card: React.FC<CardProps> = ({
variant = 'default',
className = '',
children,
onClick,
}) => {
const cardClasses = mergeClasses(
componentStyles.card.base,
variantClasses[variant],
className
);
const Component = onClick ? 'button' : 'div';
return (
<Component
className={cardClasses}
onClick={onClick}
{...(onClick ? { type: 'button' } : {})}
>
{children}
</Component>
);
};

View File

@ -1,59 +0,0 @@
import React from 'react';
import { componentStyles, mergeClasses } from '../../styles/designSystem';
import type { Service } from '../../types/service';
interface ServiceCardProps {
service: Service;
onServiceClick: (service: Service) => void;
className?: string;
}
export const ServiceCard: React.FC<ServiceCardProps> = ({
service,
onServiceClick,
className = '',
}) => {
const cardClasses = mergeClasses(
'group relative p-6 sm:p-8 transition-all duration-300 cursor-pointer',
componentStyles.card.base,
componentStyles.card.gradient,
'hover:-translate-y-4 hover:shadow-2xl hover:border-banquise-blue-lightest/50 hover:from-banquise-blue-dark/15 hover:to-banquise-blue-dark/8 active:scale-95',
className
);
const handleClick = () => {
onServiceClick(service);
};
return (
<div className={cardClasses} onClick={handleClick}>
{/* Icon */}
<div className="mb-6 sm:mb-8 w-20 h-20 sm:w-24 sm:h-24 bg-gradient-to-br from-banquise-blue to-banquise-blue-light rounded-2xl flex items-center justify-center text-3xl sm:text-4xl shadow-lg group-hover:scale-110 transition-transform duration-300 mx-auto">
<img
src={service.image}
alt={service.icon}
className="h-10 sm:h-12 lg:h-14 w-auto relative z-10 transition-transform duration-300 group-hover:scale-130 m-10"
style={
{
filter: 'drop-shadow(0 0 12px rgba(168, 218, 255, 0.4))',
}
}
/>
</div>
{/* Service name */}
<h3 className="text-xl sm:text-2xl font-bold text-banquise-gray mb-4 sm:mb-6 font-heading text-center group-hover:text-banquise-blue-lightest transition-colors duration-300">
{service.name}
</h3>
{/* Short description */}
<p className="text-banquise-gray/80 leading-relaxed text-center text-sm sm:text-base">
{service.description.split('.')[0]}.
</p>
{/* Hover effect */}
<div className="absolute inset-0 bg-gradient-to-br from-banquise-blue-lightest/10 to-banquise-blue/5 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none"></div>
</div>
);
};

View File

@ -1,83 +0,0 @@
import React from 'react';
import { URLS, SITE_CONFIG } from '../../config/constants';
export const Footer: React.FC = () => (
<footer className="bg-banquise-blue-dark/95 backdrop-blur-sm text-white py-8 px-4 sm:px-6 md:px-8 relative z-10 border-t border-banquise-blue-lightest/10 w-full box-border">
<div className="max-w-6xl mx-auto">
{/* Main Footer Content */}
<div className="flex flex-col md:flex-row justify-between items-center gap-6 mb-6">
{/* Logo/Brand */}
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-gradient-to-br from-banquise-blue-light to-banquise-blue rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-sm">B</span>
</div>
<span className="text-banquise-blue-lightest font-semibold text-lg">
{SITE_CONFIG.name}
</span>
</div>
{/* Quick Links */}
<div className="flex flex-wrap items-center gap-6 text-sm">
<a
href={URLS.services.wiki}
className="text-banquise-gray/80 hover:text-banquise-blue-lightest transition-colors duration-200"
>
Wiki
</a>
<a
href={URLS.services.gitea}
className="text-banquise-gray/80 hover:text-banquise-blue-lightest transition-colors duration-200"
>
Gitea
</a>
<a
href={URLS.services.panel}
className="text-banquise-gray/80 hover:text-banquise-blue-lightest transition-colors duration-200"
>
Panel
</a>
<a
href={URLS.services.opencloud}
className="text-banquise-gray/80 hover:text-banquise-blue-lightest transition-colors duration-200"
>
OpenCloud
</a>
</div>
{/* Social Links */}
<div className="flex items-center gap-4">
<a
href={URLS.social.discord}
className="w-10 h-10 bg-banquise-blue/20 hover:bg-banquise-blue/30 rounded-full flex items-center justify-center transition-all duration-200 hover:scale-110"
aria-label="Discord"
>
<span className="text-banquise-blue-lightest text-sm">
<svg className="w-4 h-4 lg:w-5 lg:h-5" 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>
</span>
</a>
<a
href={URLS.contact.email}
className="w-10 h-10 bg-banquise-blue/20 hover:bg-banquise-blue/30 rounded-full flex items-center justify-center transition-all duration-200 hover:scale-110"
aria-label="Email"
>
<span className="text-banquise-blue-lightest text-sm">📧</span>
</a>
</div>
</div>
{/* Bottom Bar */}
<div className="flex flex-col sm:flex-row justify-between items-center gap-4 pt-6 border-t border-banquise-blue-lightest/5">
<p className="text-banquise-gray/60 text-xs text-center sm:text-left">
© 2025 {SITE_CONFIG.name}. Hébergement communautaire pour développeurs et gamers.
</p>
<div className="flex items-center gap-4 text-xs text-banquise-gray/60">
<span>Fait avec par Banquise</span>
<div className="w-1 h-1 bg-banquise-gray/40 rounded-full"></div>
<span>EPITA 2025</span>
</div>
</div>
</div>
</footer>
);

View File

@ -1,267 +0,0 @@
import React, { useEffect } from 'react';
import { Button } from '../common/Button';
import { mergeClasses as cn } from '../../styles/designSystem';
import { Logo } from './navbar/Logo';
import { URLS } from '../../config/constants';
import type { Translation } from '../../types/i18n';
interface MobileMenuProps {
isOpen: boolean;
onClose: () => void;
translations: Translation['navigation'];
}
interface MobileNavItemProps {
icon: React.ReactNode;
title: string;
description: string;
href: string;
isExternal?: boolean;
onClick?: () => void;
}
const MobileNavItem: React.FC<MobileNavItemProps> = ({
icon,
title,
description,
href,
isExternal = false,
onClick
}) => {
const handleClick = (e: React.MouseEvent) => {
if (onClick) {
e.preventDefault();
onClick();
}
};
return (
<a
href={href}
onClick={handleClick}
className={cn(
'group flex items-center justify-between p-4 rounded-xl transition-all duration-300',
'bg-white/5 hover:bg-white/10 active:bg-white/15',
'border border-white/10 hover:border-white/20',
'hover:scale-[1.02] active:scale-[0.98]',
'hover:shadow-lg hover:shadow-banquise-blue/20'
)}
target={isExternal ? '_blank' : undefined}
rel={isExternal ? 'noopener noreferrer' : undefined}
>
<div className="flex items-center space-x-4">
<div className={cn(
'flex items-center justify-center w-10 h-10 rounded-xl',
'bg-gradient-to-br from-banquise-blue-light/20 to-banquise-blue/20',
'border border-banquise-blue-lightest/20',
'group-hover:scale-110 transition-transform duration-300'
)}>
{icon}
</div>
<div className="flex-1">
<span className="block text-white font-semibold text-base group-hover:text-banquise-blue-lightest transition-colors">
{title}
</span>
<p className="text-white/60 text-sm mt-0.5 group-hover:text-white/80 transition-colors">
{description}
</p>
</div>
</div>
{/* Arrow Icon */}
<div className={cn(
'flex items-center justify-center w-6 h-6 rounded-full',
'text-white/40 group-hover:text-white/80 transition-all duration-300',
'group-hover:translate-x-1'
)}>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</a>
);
};
export const MobileMenu: React.FC<MobileMenuProps> = ({ isOpen, onClose, translations }) => {
// Gérer le scroll du body
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'unset';
}
return () => {
document.body.style.overflow = 'unset';
};
}, [isOpen]);
const handleNavClick = (sectionId: string) => {
if (sectionId === 'home') {
// Scroll to top for home section
window.scrollTo({
top: 0,
behavior: 'smooth'
});
} else if (sectionId === 'contact') {
// Open email client for contact
window.location.href = 'mailto:contact@la-banquise.fr';
} else {
// Scroll to specific section
const element = document.getElementById(sectionId);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
}
onClose();
};
return (
<div className={cn(
'md:hidden fixed inset-0 z-[100] transition-all duration-300',
isOpen ? 'visible' : 'invisible'
)}>
{/* Overlay avec effet de blur moderne */}
<div
className={cn(
'absolute inset-0 transition-all duration-300',
'bg-gradient-to-br from-black/80 via-banquise-blue-dark/60 to-black/80',
'backdrop-blur-lg',
isOpen ? 'opacity-100' : 'opacity-0'
)}
onClick={onClose}
/>
{/* Menu Panel */}
<div className={cn(
'absolute top-0 right-0 h-full w-80 max-w-[90vw]',
'bg-gradient-to-b from-banquise-blue-dark/98 via-banquise-blue-dark/95 to-banquise-blue-dark/90',
'backdrop-blur-2xl shadow-2xl',
'border-l border-banquise-blue-lightest/20',
'transition-transform duration-300 ease-out',
isOpen ? 'translate-x-0' : 'translate-x-full'
)}>
{/* Header avec Logo */}
<div className="flex items-center justify-between p-6 pt-8 border-b border-banquise-blue-lightest/20">
<Logo scrolled={false} />
<button
className={cn(
'group relative p-3 rounded-xl transition-all duration-300',
'bg-white/10 hover:bg-white/20 active:bg-white/25',
'border border-white/20 hover:border-white/30',
'hover:scale-105 active:scale-95',
'focus:outline-none focus:ring-2 focus:ring-banquise-blue-light/50'
)}
onClick={onClose}
aria-label="Fermer le menu"
>
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Navigation Items */}
<div className="flex flex-col h-full overflow-y-auto p-6 space-y-4">
{/* Section Navigation */}
<div className="space-y-3">
<MobileNavItem
icon={
<svg className="w-5 h-5 text-banquise-blue-lightest" 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}
description="Retour à l'accueil"
href="#home"
onClick={() => handleNavClick('home')}
/>
<MobileNavItem
icon={
<svg className="w-5 h-5 text-banquise-blue-lightest" 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}
description="Découvrir notre offre"
href="#services"
onClick={() => handleNavClick('services')}
/>
<MobileNavItem
icon={
<svg className="w-5 h-5 text-banquise-blue-lightest" 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}
description="En savoir plus sur nous"
href="#about"
onClick={() => handleNavClick('about')}
/>
<MobileNavItem
icon={
<svg className="w-5 h-5 text-banquise-blue-lightest" 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}
description="Nous envoyer un email"
href="mailto:contact@la-banquise.fr"
onClick={() => handleNavClick('contact')}
/>
</div>
{/* Divider */}
<div className="border-t border-banquise-blue-lightest/20 my-6" />
{/* Social & External Links */}
<div className="space-y-3">
<MobileNavItem
icon={
<svg className="w-5 h-5 text-[#5865F2]" fill="currentColor" viewBox="0 0 24 24">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.211.375-.445.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03z"/>
</svg>
}
title="Discord"
description="Rejoindre la communauté"
href={URLS.social.discord}
isExternal={true}
/>
</div>
{/* CTA Button */}
<div className="mt-8 pb-6">
<Button
variant="primary"
size="lg"
leftIcon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
}
onClick={() => {
window.open(URLS.services.auth, '_blank');
onClose();
}}
className="w-full shadow-xl"
>
Se connecter
</Button>
</div>
</div>
{/* Effet de gradient overlay */}
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-banquise-blue-dark/10 pointer-events-none" />
</div>
</div>
);
};

View File

@ -1,120 +0,0 @@
import React from 'react';
import { useScrollEffects } from '../../hooks/useScrollEffects';
import { mergeClasses as cn } from '../../styles/designSystem';
import { Logo } from './navbar/Logo';
import { NavLinks } from './navbar/NavLinks';
import { ActionButtons } from './navbar/ActionButtons';
import { MobileMenuButton } from './navbar/MobileMenuButton';
import { MobileMenu } from './MobileMenu';
import type { Translation } from '../../types/i18n';
interface ModernNavigationProps {
translations: Translation['navigation'];
languageSwitcher: React.ReactElement;
}
export const ModernNavigation: React.FC<ModernNavigationProps> = ({
translations,
languageSwitcher
}) => {
const { scrolled } = useScrollEffects();
const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);
// Fermer le menu mobile lors du redimensionnement
React.useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= 768) {
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 (
<>
{/* Navigation Bar */}
<nav
className={cn(
'fixed top-0 left-0 right-0 z-50 transition-all duration-500 ease-out',
// Background adaptatif selon le scroll
scrolled
? 'bg-banquise-blue-dark/95 backdrop-blur-xl shadow-2xl border-b border-banquise-blue-lightest/30'
: 'bg-banquise-blue-dark/90 backdrop-blur-lg shadow-xl border-b border-banquise-blue-lightest/20',
// Animation de hauteur
'will-change-auto'
)}
>
<div className="max-w-7xl mx-auto">
<div className={cn(
'flex justify-between items-center px-4 sm:px-6 lg:px-8 transition-all duration-300',
scrolled ? 'h-16' : 'h-18 lg:h-20'
)}>
{/* Logo Section */}
<Logo scrolled={scrolled} />
{/* Navigation Links (Desktop) */}
<NavLinks
translations={translations}
scrolled={scrolled}
className="flex-1 justify-center ml-8"
/>
{/* Action Buttons (Desktop) */}
<ActionButtons
scrolled={scrolled}
languageSwitcher={languageSwitcher}
/>
{/* Mobile Menu Button */}
<MobileMenuButton
isOpen={mobileMenuOpen}
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
/>
</div>
</div>
{/* Effet de bordure dégradée moderne */}
<div className="absolute bottom-0 left-0 right-0">
<div className="h-px bg-gradient-to-r from-transparent via-banquise-blue-lightest/40 to-transparent" />
<div className="h-px bg-gradient-to-r from-transparent via-banquise-blue-light/20 to-transparent blur-sm" />
</div>
{/* Effet de glow subtil */}
<div className={cn(
'absolute inset-0 pointer-events-none transition-opacity duration-500',
'bg-gradient-to-b from-banquise-blue-light/5 to-transparent',
scrolled ? 'opacity-60' : 'opacity-100'
)} />
</nav>
{/* Spacer pour compenser la navbar fixed */}
<div className={cn(
'transition-all duration-300',
scrolled ? 'h-16' : 'h-18 lg:h-20'
)} />
{/* Menu Mobile */}
<MobileMenu
isOpen={mobileMenuOpen}
onClose={() => setMobileMenuOpen(false)}
translations={translations}
/>
</>
);
};

View File

@ -1,159 +0,0 @@
import React, { useState, useEffect } from 'react';
import { MobileMenu } from './MobileMenu';
import banquiseServer from '/src/assets/banquise_server.svg'
import { URLS, SITE_CONFIG } from '../../config/constants';
import { commonStyles } from '../../styles/components';
import type { Translation } from '../../types/i18n';
interface NavigationProps {
translations: Translation['navigation'];
languageSwitcher: React.ReactElement;
}
export const Navigation: React.FC<NavigationProps> = ({ translations, languageSwitcher }) => {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const handleScroll = () => {
const isScrolled = window.scrollY > 20;
setScrolled(isScrolled);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= 768) {
setMobileMenuOpen(false);
}
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return (
<>
<nav className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
scrolled
? 'bg-banquise-blue-dark/98 backdrop-blur-xl shadow-2xl border-b border-banquise-blue-lightest/30'
: 'bg-banquise-blue-dark/95 backdrop-blur-lg shadow-xl border-b border-banquise-blue-lightest/20'
}`}>
<div className={commonStyles.layout.container}>
<div className="flex justify-between items-center h-16 sm:h-18 lg:h-20 px-4 sm:px-6 lg:px-8">
{/* Logo section */}
<div className="flex items-center space-x-3 sm:space-x-4 group">
<div className="relative">
<div className="absolute inset-0 bg-gradient-to-r from-banquise-blue-light/20 to-banquise-blue/20 rounded-full blur-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<img
src={banquiseServer}
alt="Logo La Banquise"
className="h-10 sm:h-12 lg:h-14 w-auto relative z-10 transition-transform duration-300 group-hover:scale-110"
style={{ filter: 'drop-shadow(0 0 12px rgba(168, 218, 255, 0.4))' }}
/>
</div>
<div className="hidden sm:block">
<h1 className={`text-xl sm:text-2xl lg:text-3xl font-bold text-white tracking-wide ${commonStyles.text.heading}`}>
{SITE_CONFIG.name}
</h1>
<p className="text-banquise-blue-lightest/80 text-xs lg:text-sm font-medium">
{SITE_CONFIG.tagline}
</p>
</div>
</div>
{/* Navigation links desktop */}
<div className="hidden md:flex items-center space-x-1 lg:space-x-2">
<a href="#home" className={commonStyles.nav.link}>
<span className="relative z-10">{translations.home}</span>
<div className="absolute inset-0 bg-gradient-to-r from-banquise-blue-light/20 to-banquise-blue/20 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
</a>
<a href="#services" className={commonStyles.nav.link}>
<span className="relative z-10">{translations.services}</span>
<div className="absolute inset-0 bg-gradient-to-r from-banquise-blue-light/20 to-banquise-blue/20 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
</a>
<a href="#about" className={commonStyles.nav.link}>
<span className="relative z-10">{translations.about}</span>
<div className="absolute inset-0 bg-gradient-to-r from-banquise-blue-light/20 to-banquise-blue/20 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
</a>
<a href="#contact" className={commonStyles.nav.link}>
<span className="relative z-10">{translations.contact}</span>
<div className="absolute inset-0 bg-gradient-to-r from-banquise-blue-light/20 to-banquise-blue/20 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
</a>
</div>
{/* Action buttons desktop */}
<div className="hidden md:flex items-center space-x-3 lg:space-x-4">
{/* Language switcher */}
{languageSwitcher}
<a
href={URLS.social.discord}
target="_blank"
rel="noopener noreferrer"
className={`${commonStyles.buttons.discord} ${commonStyles.gradients.discord}`}
>
<div className={`absolute inset-0 ${commonStyles.gradients.discordHover} opacity-0 group-hover:opacity-100 transition-opacity duration-300`}></div>
<div className="relative z-10 flex items-center space-x-2">
<svg className="w-4 h-4 lg:w-5 lg:h-5" 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>
<span>Discord</span>
</div>
</a>
<a
href={URLS.services.auth}
target="_blank"
rel="noopener noreferrer"
className={`${commonStyles.buttons.auth} ${
scrolled
? `${commonStyles.gradients.primary} border border-banquise-blue-lightest/30 hover:shadow-banquise-blue/25`
: 'bg-gradient-to-r from-banquise-blue-light to-banquise-blue border-2 border-white/20 hover:shadow-banquise-blue-light/25'
}`}
>
<div className={`absolute inset-0 transition-opacity duration-300 opacity-0 group-hover:opacity-100 ${
scrolled
? 'bg-gradient-to-r from-banquise-blue-light to-banquise-blue'
: 'bg-gradient-to-r from-white/10 to-banquise-blue-lightest/20'
}`}></div>
<div className="relative z-10 flex items-center space-x-2">
<svg className="w-4 h-4 lg:w-5 lg: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>
<span className="hidden lg:inline">Connexion</span>
</div>
</a>
</div>
{/* Mobile menu button */}
<button
className="md:hidden relative p-3 rounded-xl bg-white/10 hover:bg-white/20 transition-all duration-300 hover:scale-105 active:scale-95 group"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-label={mobileMenuOpen ? "Fermer le menu" : "Ouvrir le menu"}
aria-expanded={mobileMenuOpen}
>
<div className="w-6 h-6 relative">
<span className={`absolute block w-6 h-6 bg-white transition-all duration-300 ${mobileMenuOpen ? 'rotate-45 top-3' : 'top-1'}`}></span>
<span className={`absolute block w-6 h-0.5 bg-white transition-all duration-300 top-3 ${mobileMenuOpen ? 'opacity-0 scale-0' : 'opacity-100'}`}></span>
<span className={`absolute block w-6 h-0.5 bg-white transition-all duration-300 ${mobileMenuOpen ? '-rotate-45 top-3' : 'top-5'}`}></span>
</div>
</button>
</div>
</div>
{/* Glassmorphism effect bar */}
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-banquise-blue-lightest/30 to-transparent"></div>
</nav>
{/* Spacer pour compenser la navbar fixed */}
<div className="h-16 sm:h-18 lg:h-20"></div>
{/* Menu mobile */}
<MobileMenu
isOpen={mobileMenuOpen}
onClose={() => setMobileMenuOpen(false)}
translations={translations}
/>
</>
);
};

View File

@ -1,74 +0,0 @@
import React from 'react';
import { Button } from '../../common/Button';
import { mergeClasses as cn } from '../../../styles/designSystem';
import { URLS } from '../../../config/constants';
interface ActionButtonsProps {
scrolled: boolean;
languageSwitcher: React.ReactElement;
className?: string;
}
export const ActionButtons: React.FC<ActionButtonsProps> = ({
scrolled,
languageSwitcher,
className
}) => {
return (
<div className={cn('hidden md:flex items-center space-x-3', className)}>
{/* Language Switcher */}
<div className="relative">
{languageSwitcher}
</div>
{/* Discord Button */}
<Button
variant="discord"
size="sm"
leftIcon={
<svg className="w-4 h-4" 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>
}
onClick={() => window.open(URLS.social.discord, '_blank')}
className="hidden lg:flex"
>
Discord
</Button>
{/* Discord Icon Only (tablet) */}
<Button
variant="discord"
size="sm"
onClick={() => window.open(URLS.social.discord, '_blank')}
className="lg:hidden"
aria-label="Rejoindre Discord"
>
<svg className="w-4 h-4" 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>
</Button>
{/* Auth Button */}
<Button
variant="primary"
size="sm"
leftIcon={
<svg className="w-4 h-4" 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={() => window.open(URLS.services.auth, '_blank')}
className={cn(
'transition-all duration-300',
scrolled
? 'shadow-md hover:shadow-lg'
: 'shadow-lg hover:shadow-xl'
)}
>
<span className="hidden lg:inline">Connexion</span>
<span className="lg:hidden">Se connecter</span>
</Button>
</div>
);
};

View File

@ -1,52 +0,0 @@
import React from 'react';
import { mergeClasses as cn } from '../../../styles/designSystem';
import banquiseServer from '/src/assets/banquise_server.svg';
import { SITE_CONFIG } from '../../../config/constants';
interface LogoProps {
scrolled: boolean;
className?: string;
}
export const Logo: React.FC<LogoProps> = ({ scrolled, className }) => {
return (
<div className={cn('flex items-center group cursor-pointer', className)}>
{/* Logo avec effet glow */}
<div className="relative flex items-center">
<div className="absolute inset-0 bg-gradient-to-r from-banquise-blue-light/30 to-banquise-blue/30 rounded-full blur-md opacity-0 group-hover:opacity-100 transition-all duration-300 scale-110"></div>
<div className={cn(
'relative flex items-center justify-center rounded-full p-2 bg-white/10 backdrop-blur-sm border border-white/20 transition-all duration-300',
'group-hover:bg-white/20 group-hover:scale-105 group-hover:border-white/30'
)}>
<img
src={banquiseServer}
alt="Logo La Banquise"
className={cn(
'transition-all duration-300 group-hover:scale-110',
scrolled ? 'h-8 w-8' : 'h-10 w-10'
)}
style={{ filter: 'drop-shadow(0 4px 12px rgba(168, 218, 255, 0.4))' }}
/>
</div>
</div>
{/* Brand text avec animation */}
<div className="ml-3 hidden sm:block">
<h1 className={cn(
'font-heading font-bold text-white tracking-tight transition-all duration-300',
scrolled ? 'text-lg' : 'text-xl lg:text-2xl',
'group-hover:text-banquise-blue-lightest'
)}>
{SITE_CONFIG.name}
</h1>
<p className={cn(
'text-banquise-blue-lightest/70 font-medium transition-all duration-300',
scrolled ? 'text-xs' : 'text-sm',
'group-hover:text-banquise-blue-lightest/90'
)}>
{SITE_CONFIG.tagline}
</p>
</div>
</div>
);
};

View File

@ -1,174 +0,0 @@
import React from 'react';
import { mergeClasses as cn } from '../../../styles/designSystem';
import type { Translation } from '../../../types/i18n';
interface NavLinksProps {
translations: Translation['navigation'];
scrolled: boolean;
className?: string;
}
interface NavLinkProps {
href: string;
children: React.ReactNode;
isActive?: boolean;
onClick?: () => void;
}
const NavLink: React.FC<NavLinkProps> = ({ href, children, isActive = false, onClick }) => {
return (
<a
href={href}
onClick={onClick}
className={cn(
'relative px-4 py-2 text-sm font-medium transition-all duration-300 rounded-lg group',
'hover:text-white focus:outline-none focus:ring-2 focus:ring-banquise-blue-light/50',
isActive
? 'text-white bg-white/20 shadow-lg'
: 'text-white/80 hover:bg-white/10'
)}
>
<span className="relative z-10">{children}</span>
{/* Hover effect */}
<div className={cn(
'absolute inset-0 rounded-lg bg-gradient-to-r from-banquise-blue-light/20 to-banquise-blue/20',
'opacity-0 group-hover:opacity-100 transition-all duration-300 scale-95 group-hover:scale-100'
)} />
{/* Active indicator */}
{isActive && (
<div className="absolute bottom-0 left-1/2 transform -translate-x-1/2 w-6 h-0.5 bg-banquise-blue-lightest rounded-full" />
)}
</a>
);
};
export const NavLinks: React.FC<NavLinksProps> = ({ translations, className }) => {
const [activeSection, setActiveSection] = React.useState<string>('home');
// Observer pour détecter la section active
React.useEffect(() => {
const handleScroll = () => {
const scrollPosition = window.scrollY;
const windowHeight = window.innerHeight;
// Si on est en haut de la page (moins de 100px du haut), on active "home"
if (scrollPosition < 100) {
setActiveSection('home');
return;
}
// Sinon, on utilise l'intersection observer logic
const sections = ['home', 'services', 'about'];
let currentSection = 'home';
sections.forEach((sectionId) => {
const element = document.getElementById(sectionId);
if (element) {
const rect = element.getBoundingClientRect();
const sectionTop = rect.top + scrollPosition;
// Si la section est visible dans le viewport
if (scrollPosition >= sectionTop - windowHeight / 3) {
currentSection = sectionId;
}
}
});
setActiveSection(currentSection);
};
// Écouter le scroll
window.addEventListener('scroll', handleScroll);
// Appeler une fois au chargement
handleScroll();
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
// Observer pour détecter la section active avec IntersectionObserver (fallback)
React.useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && window.scrollY > 100) {
setActiveSection(entry.target.id);
}
});
},
{
threshold: 0.3,
rootMargin: '-100px 0px -100px 0px'
}
);
const sections = ['home', 'services', 'about'];
sections.forEach((id) => {
const element = document.getElementById(id);
if (element) observer.observe(element);
});
return () => observer.disconnect();
}, []);
const handleNavClick = (sectionId: string) => {
if (sectionId === 'home') {
// Scroll to top for home section
window.scrollTo({
top: 0,
behavior: 'smooth'
});
} else if (sectionId === 'contact') {
// Open email client for contact
window.location.href = 'mailto:contact@la-banquise.fr';
} else {
// Scroll to specific section
const element = document.getElementById(sectionId);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
}
};
return (
<nav className={cn('hidden md:flex items-center space-x-1', className)}>
<NavLink
href="#home"
isActive={activeSection === 'home'}
onClick={() => handleNavClick('home')}
>
{translations.home}
</NavLink>
<NavLink
href="#services"
isActive={activeSection === 'services'}
onClick={() => handleNavClick('services')}
>
{translations.services}
</NavLink>
<NavLink
href="#about"
isActive={activeSection === 'about'}
onClick={() => handleNavClick('about')}
>
{translations.about}
</NavLink>
<NavLink
href="mailto:contact@la-banquise.fr"
isActive={false}
onClick={() => handleNavClick('contact')}
>
{translations.contact}
</NavLink>
</nav>
);
};

View File

@ -1,155 +0,0 @@
import React from 'react';
import { AccordionItem } from '../ui/AccordionItem';
import { URLS } from '../../config/constants';
import { commonStyles } from '../../styles/components';
interface AboutSectionProps {
openAccordion: string | null;
toggleAccordion: (title: string) => void;
}
export const AboutSection: React.FC<AboutSectionProps> = ({ openAccordion, toggleAccordion }) => (
<section id="about" className="relative py-16 sm:py-20 md:py-24 px-4 sm:px-6 md:px-8 z-2 w-full box-border">
<div className="max-w-4xl mx-auto">
{/* Header */}
<div className="text-center mb-12 sm:mb-16 md:mb-20">
<h2 className={`${commonStyles.text.headingXl} mb-6 sm:mb-8 px-2`} style={{ textShadow: '0 2px 4px rgba(0, 0, 0, 0.2)' }}>
À Propos de La Banquise
</h2>
<p className={`${commonStyles.text.muted} text-lg sm:text-xl max-w-3xl mx-auto px-2`} style={{ textShadow: '0 1px 2px rgba(0, 0, 0, 0.1)' }}>
Une communauté passionnée qui propose des services d'hébergement et des outils collaboratifs pour les développeurs et les gamers.
</p>
</div>
{/* FAQ Section */}
<div className="space-y-4 sm:space-y-6">
<h3 className={`${commonStyles.text.headingLg} mb-8 sm:mb-12 flex items-center justify-center px-2`}>
<span className="text-2xl sm:text-3xl mr-3"></span>
<span className="text-center">Questions Fréquentes</span>
</h3>
<AccordionItem
title="🎯 Notre Mission"
isOpen={openAccordion === "mission"}
onToggle={() => toggleAccordion("mission")}
>
<div className="space-y-4">
<p className={commonStyles.text.muted}>
Former les étudiants au déploiment et a la gestion d'une infra, et de maitriser des technologies entreprise grade.
Cela permet de fournir une plateforme stable et accessible pour héberger vos projets, partager vos connaissances et jouer ensemble !
</p>
<p className={commonStyles.text.muted}>
Nous croyons en la puissance de la collaboration et mettons à disposition des outils modernes pour faciliter le travail en équipe.
</p>
<div className="flex flex-wrap gap-2 mt-4">
<span className="bg-banquise-blue/20 text-banquise-blue-light px-3 py-1 rounded-full text-sm font-medium">Collaboration</span>
<span className="bg-banquise-blue/20 text-banquise-blue-light px-3 py-1 rounded-full text-sm font-medium">Innovation</span>
<span className="bg-banquise-blue/20 text-banquise-blue-light px-3 py-1 rounded-full text-sm font-medium">Accessibilité</span>
</div>
</div>
</AccordionItem>
<AccordionItem
title="🛠️ Nos Services"
isOpen={openAccordion === "services"}
onToggle={() => toggleAccordion("services")}
>
<div className="space-y-6">
<div className="grid gap-4">
<div className={`flex items-start space-x-4 p-4 ${commonStyles.gradients.card} rounded-xl ${commonStyles.cards.base}`}>
<div className={`${commonStyles.icons.small} ${commonStyles.gradients.primaryBr} font-bold`}>📚</div>
<div>
<h4 className="font-semibold text-banquise-gray mb-1">Wiki</h4>
<p className="text-banquise-gray/80 text-sm">Documentation collaborative et guides détaillés</p>
</div>
</div>
<div className={`flex items-start space-x-4 p-4 ${commonStyles.gradients.card} rounded-xl ${commonStyles.cards.base}`}>
<div className={`${commonStyles.icons.small} ${commonStyles.gradients.primaryBr} font-bold`}>🔧</div>
<div>
<h4 className="font-semibold text-banquise-gray mb-1">Gitea</h4>
<p className="text-banquise-gray/80 text-sm">Gestion de versions Git auto-hébergée</p>
</div>
</div>
<div className={`flex items-start space-x-4 p-4 ${commonStyles.gradients.card} rounded-xl ${commonStyles.cards.base}`}>
<div className={`${commonStyles.icons.small} ${commonStyles.gradients.primaryBr} font-bold`}>🎮</div>
<div>
<h4 className="font-semibold text-banquise-gray mb-1">Panel de Jeux</h4>
<p className="text-banquise-gray/80 text-sm">Interface de gestion pour serveurs de jeux</p>
</div>
</div>
<div className={`flex items-start space-x-4 p-4 ${commonStyles.gradients.card} rounded-xl ${commonStyles.cards.base}`}>
<div className={`${commonStyles.icons.small} ${commonStyles.gradients.primaryBr} font-bold`}>🐧</div>
<div>
<h4 className="font-semibold text-banquise-gray mb-1">Pelican</h4>
<p className="text-banquise-gray/80 text-sm">Générateur de sites statiques</p>
</div>
</div>
<div className={`flex items-start space-x-4 p-4 ${commonStyles.gradients.card} rounded-xl ${commonStyles.cards.base}`}>
<div className={`${commonStyles.icons.small} ${commonStyles.gradients.primaryBr} font-bold`}>🏢</div>
<div>
<h4 className="font-semibold text-banquise-gray mb-1">Intranet</h4>
<p className="text-banquise-gray/80 text-sm">Espace privé de l'association</p>
</div>
</div>
<div className={`flex items-start space-x-4 p-4 ${commonStyles.gradients.card} rounded-xl ${commonStyles.cards.base}`}>
<div className={`${commonStyles.icons.small} ${commonStyles.gradients.primaryBr} font-bold`}>📧</div>
<div>
<h4 className="font-semibold text-banquise-gray mb-1">Webmail</h4>
<p className="text-banquise-gray/80 text-sm">Service de messagerie électronique</p>
</div>
</div>
<div className={`flex items-start space-x-4 p-4 ${commonStyles.gradients.card} rounded-xl ${commonStyles.cards.base}`}>
<div className={`${commonStyles.icons.small} ${commonStyles.gradients.primaryBr} font-bold`}></div>
<div>
<h4 className="font-semibold text-banquise-gray mb-1">OpenCloud</h4>
<p className="text-banquise-gray/80 text-sm">Plateforme cloud collaborative</p>
</div>
</div>
</div>
<p className={`${commonStyles.text.muted} mt-4`}>
Tous nos services sont maintenus avec soin et régulièrement mis à jour pour garantir une expérience optimale.
</p>
</div>
</AccordionItem>
<AccordionItem
title="🤝 Rejoindre l'association"
isOpen={openAccordion === "community"}
onToggle={() => toggleAccordion("community")}
>
<div className="space-y-6">
<p className={commonStyles.text.muted}>
Rejoignez notre serveur Discord pour rejoindre l'asso, échanger avec nous, obtenir de l'aide et rester informé des dernières nouveautés !
</p>
<div className={`${commonStyles.cards.base} bg-gradient-to-r from-banquise-blue-dark/20 to-banquise-blue/10 rounded-2xl p-6`}>
<h4 className="font-semibold text-banquise-gray mb-3 flex items-center">
<span className="text-xl mr-2">💬</span>
Comment rejoindre l'asso ?
</h4>
<ul className="space-y-2 text-banquise-gray/80 text-sm mb-6">
<li className="flex items-center"><span className="text-banquise-blue-light mr-2"></span> Creez un ticket banquise</li>
<li className="flex items-center"><span className="text-banquise-blue-light mr-2"></span> Donnez votre login EPITA ou expliquez votre situation</li>
<li className="flex items-center"><span className="text-banquise-blue-light mr-2"></span> Un moderateur validera votre demande et vous donnera acces aux salons discord de l'asso !</li>
</ul>
<a
href={URLS.social.discord}
className={`${commonStyles.buttons.primary} ${commonStyles.gradients.primary} py-3 px-6 rounded-xl`}
>
<span className="mr-3 text-lg">🚀</span>
Rejoindre Discord
</a>
</div>
</div>
</AccordionItem>
</div>
</div>
</section>
);

View File

@ -1,35 +0,0 @@
import React from 'react';
import banquiseServer from '/src/assets/banquise_server.svg'
import type { Translation } from '../../types/i18n';
interface HeroSectionProps {
translations: Translation['hero'];
}
export const HeroSection: React.FC<HeroSectionProps> = ({ translations }) => (
<section id="home" className="min-h-[calc(80vh-72px)] flex flex-col justify-center items-center text-center py-12 sm:py-16 md:py-20 w-full max-w-6xl mx-auto px-4 sm:px-6 md:px-8 relative z-3">
<div className="mb-8 sm:mb-10 md:mb-12 w-32 h-32 sm:w-40 sm:h-40 md:w-48 md:h-48 rounded-full bg-gradient-to-br from-banquise-blue-dark/20 to-banquise-blue/10 p-4 sm:p-5 md:p-6 shadow-2xl backdrop-blur-sm border border-banquise-blue-lightest/30 relative group">
<img
src={banquiseServer}
alt="Logo La Banquise"
className="w-full h-full object-contain relative z-10 transition-transform duration-300 group-hover:scale-110"
style={{
filter: 'drop-shadow(0 10px 25px rgba(31, 93, 137, 0.3))'
}}
/>
</div>
<h1 className="text-banquise-gray text-3xl sm:text-4xl md:text-5xl lg:text-6xl mb-6 sm:mb-7 md:mb-8 font-extrabold leading-tight max-w-4xl font-heading px-2 relative z-10" style={{ textShadow: '0 2px 10px rgba(0, 0, 0, 0.3)' }}>
{translations.title}
</h1>
<p className="text-banquise-gray text-lg sm:text-xl md:text-2xl mb-8 sm:mb-10 md:mb-12 max-w-3xl font-normal opacity-90 leading-relaxed px-2 relative z-10" style={{ textShadow: '0 1px 4px rgba(0, 0, 0, 0.2)' }}>
{translations.subtitle}
</p>
<a href="#services" className="inline-flex items-center justify-center bg-gradient-to-r from-banquise-gray to-white text-banquise-blue-dark border-0 py-4 sm:py-5 px-8 sm:px-10 md:px-12 rounded-2xl text-base sm:text-lg font-bold no-underline shadow-xl transition-all duration-300 min-w-48 sm:min-w-56 md:min-w-64 hover:-translate-y-2 hover:shadow-2xl hover:scale-105 backdrop-blur-sm border border-banquise-blue-lightest/20 mx-4 group relative z-10">
<span className="text-center text-banquise-blue-dark">{translations.cta}</span>
<span className="ml-2 sm:ml-3 text-lg sm:text-xl transition-transform duration-300 group-hover:translate-x-1 text-banquise-blue-dark"></span>
</a>
</section>
);

View File

@ -1,37 +0,0 @@
import React from 'react';
import { ServiceCard } from '../common/ServiceCard';
//import { componentStyles } from '../../styles/designSystem';
import type { Service } from '../../types/service';
interface ServicesSectionProps {
services: Service[];
onServiceClick: (service: Service) => void;
translations: {
discoverFeatures: string;
};
}
export const ServicesSection: React.FC<ServicesSectionProps> = ({
services,
onServiceClick
}) => (
<section id="services" className="relative z-2 py-12 sm:py-16 md:py-20 w-full max-w-6xl mx-auto px-4 sm:px-6 md:px-8">
<div className="w-20 h-1 bg-gradient-to-r from-banquise-blue-lightest to-banquise-blue mx-auto mb-6 sm:mb-8 rounded-full"></div>
<h2 className="text-banquise-gray text-2xl sm:text-3xl md:text-4xl mb-4 sm:mb-6 text-center font-heading font-bold tracking-tight px-2" style={{ textShadow: '0 2px 4px rgba(0, 0, 0, 0.2)' }}>
Nos Services
</h2>
<p className="text-banquise-gray text-lg sm:text-xl opacity-90 mb-12 sm:mb-14 md:mb-16 max-w-4xl text-center mx-auto leading-relaxed px-2" style={{ textShadow: '0 1px 3px rgba(0, 0, 0, 0.2)' }}>
Cliquez sur un service pour découvrir toutes ses fonctionnalités
</p>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 sm:gap-8 w-full">
{services.map((service) => (
<ServiceCard
key={service.name}
service={service}
onServiceClick={onServiceClick}
/>
))}
</div>
</section>
);

View File

@ -1,47 +0,0 @@
import React from 'react';
export const TechFeaturesSection: React.FC = () => (
<section className="py-12 sm:py-16 md:py-20 relative z-2 w-full max-w-6xl mx-auto px-4 sm:px-6 md:px-8">
<div className="w-20 h-1 bg-gradient-to-r from-banquise-blue-lightest to-banquise-blue mx-auto mb-6 sm:mb-8 rounded-full"></div>
<h2 className="text-banquise-gray text-2xl sm:text-3xl md:text-4xl mb-4 sm:mb-6 text-center font-heading font-bold tracking-tight px-2" style={{ textShadow: '0 2px 4px rgba(0, 0, 0, 0.2)' }}>
Notre Infrastructure
</h2>
<p className="text-banquise-gray text-lg sm:text-xl opacity-90 mb-12 sm:mb-14 md:mb-16 max-w-4xl text-center mx-auto leading-relaxed px-2" style={{ textShadow: '0 1px 3px rgba(0, 0, 0, 0.2)' }}>
25+ serveurs pour répondre à vos besoins
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4 sm:gap-6 w-full">
<div className="bg-gradient-to-br from-banquise-blue-dark/10 to-banquise-blue-dark/5 backdrop-blur-lg rounded-2xl p-6 sm:p-8 flex flex-col items-center text-center transition-all duration-300 border border-banquise-blue-lightest/30 hover:-translate-y-3 hover:from-banquise-blue-dark/15 hover:to-banquise-blue-dark/8 hover:shadow-xl hover:border-banquise-blue-lightest/50 group">
<div className="text-3xl sm:text-4xl mb-4 sm:mb-6 text-white bg-gradient-to-br from-banquise-blue to-banquise-blue-light w-16 h-16 sm:w-20 sm:h-20 flex items-center justify-center rounded-2xl shadow-lg group-hover:scale-110 transition-transform duration-300">
🚀
</div>
<h3 className="text-lg sm:text-xl mb-3 sm:mb-4 text-banquise-gray font-heading font-semibold group-hover:text-banquise-blue-lightest transition-colors duration-300">Serveurs performants</h3>
<p className="text-banquise-gray/80 leading-relaxed text-sm">Infrastructure optimisée pour assurer des performances élevées et une disponibilité maximale de vos applications</p>
</div>
<div className="bg-gradient-to-br from-banquise-blue-dark/10 to-banquise-blue-dark/5 backdrop-blur-lg rounded-2xl p-6 sm:p-8 flex flex-col items-center text-center transition-all duration-300 border border-banquise-blue-lightest/30 hover:-translate-y-3 hover:from-banquise-blue-dark/15 hover:to-banquise-blue-dark/8 hover:shadow-xl hover:border-banquise-blue-lightest/50 group">
<div className="text-3xl sm:text-4xl mb-4 sm:mb-6 text-white bg-gradient-to-br from-banquise-blue to-banquise-blue-light w-16 h-16 sm:w-20 sm:h-20 flex items-center justify-center rounded-2xl shadow-lg group-hover:scale-110 transition-transform duration-300">
💾
</div>
<h3 className="text-lg sm:text-xl mb-3 sm:mb-4 text-banquise-gray font-heading font-semibold group-hover:text-banquise-blue-lightest transition-colors duration-300">Stockage sécurisé</h3>
<p className="text-banquise-gray/80 leading-relaxed text-sm">Solutions de stockage distribuées avec redondance pour garantir l'intégrité et la durabilité de vos données</p>
</div>
<div className="bg-gradient-to-br from-banquise-blue-dark/10 to-banquise-blue-dark/5 backdrop-blur-lg rounded-2xl p-6 sm:p-8 flex flex-col items-center text-center transition-all duration-300 border border-banquise-blue-lightest/30 hover:-translate-y-3 hover:from-banquise-blue-dark/15 hover:to-banquise-blue-dark/8 hover:shadow-xl hover:border-banquise-blue-lightest/50 group">
<div className="text-3xl sm:text-4xl mb-4 sm:mb-6 text-white bg-gradient-to-br from-banquise-blue to-banquise-blue-light w-16 h-16 sm:w-20 sm:h-20 flex items-center justify-center rounded-2xl shadow-lg group-hover:scale-110 transition-transform duration-300">
🌐
</div>
<h3 className="text-lg sm:text-xl mb-3 sm:mb-4 text-banquise-gray font-heading font-semibold group-hover:text-banquise-blue-lightest transition-colors duration-300">Réseau optimisé</h3>
<p className="text-banquise-gray/80 leading-relaxed text-sm">Architecture réseau à haute disponibilité avec une faible latence pour vos applications critiques</p>
</div>
<div className="bg-gradient-to-br from-banquise-blue-dark/10 to-banquise-blue-dark/5 backdrop-blur-lg rounded-2xl p-6 sm:p-8 flex flex-col items-center text-center transition-all duration-300 border border-banquise-blue-lightest/30 hover:-translate-y-3 hover:from-banquise-blue-dark/15 hover:to-banquise-blue-dark/8 hover:shadow-xl hover:border-banquise-blue-lightest/50 group">
<div className="text-3xl sm:text-4xl mb-4 sm:mb-6 text-white bg-gradient-to-br from-banquise-blue to-banquise-blue-light w-16 h-16 sm:w-20 sm:h-20 flex items-center justify-center rounded-2xl shadow-lg group-hover:scale-110 transition-transform duration-300">
🛡
</div>
<h3 className="text-lg sm:text-xl mb-3 sm:mb-4 text-banquise-gray font-heading font-semibold group-hover:text-banquise-blue-lightest transition-colors duration-300">Sécurité renforcée</h3>
<p className="text-banquise-gray/80 leading-relaxed text-sm">Protection contre les menaces avec systèmes de sécurité modernes et mises à jour régulières</p>
</div>
</div>
</section>
);

View File

@ -1,21 +0,0 @@
import React from 'react';
import type { AccordionItemProps } from '../../types';
export const AccordionItem: React.FC<AccordionItemProps> = ({ title, children, isOpen, onToggle }) => (
<div className={`bg-gradient-to-br from-banquise-blue-dark/15 to-banquise-blue-dark/5 backdrop-blur-lg rounded-2xl overflow-hidden border border-banquise-blue-lightest/30 transition-all duration-300 shadow-sm ${isOpen ? 'shadow-xl border-banquise-blue-lightest/50 scale-[1.01]' : ''} hover:shadow-lg hover:border-banquise-blue-lightest/40`}>
<div
className="p-4 sm:p-6 md:p-8 cursor-pointer flex items-center justify-between font-semibold text-banquise-gray transition-all duration-200 text-base sm:text-lg select-none hover:bg-banquise-blue-dark/10 active:bg-banquise-blue-dark/15"
onClick={onToggle}
>
<span className="flex items-center flex-1 mr-4 font-heading">{title}</span>
<span className={`text-xl sm:text-2xl transition-transform duration-300 text-banquise-blue-lightest flex-shrink-0 ${isOpen ? 'rotate-180' : ''}`}>
</span>
</div>
<div className={`transition-all duration-500 overflow-hidden ${isOpen ? 'max-h-[1000px] pb-4 px-4 sm:pb-6 sm:px-6 md:pb-8 md:px-8' : 'max-h-0'}`}>
<div className="text-banquise-gray/90 leading-relaxed text-sm sm:text-base">
{children}
</div>
</div>
</div>
);

View File

@ -1,37 +0,0 @@
import React from 'react';
import type { Language } from '../../types/i18n';
interface LanguageSwitcherProps {
currentLanguage: Language;
onLanguageChange: (language: Language) => void;
availableLanguages: Language[];
}
export const LanguageSwitcher: React.FC<LanguageSwitcherProps> = ({
currentLanguage,
onLanguageChange,
availableLanguages
}) => {
const languageNames: Record<Language, string> = {
fr: '🇫🇷 Français',
en: '🇬🇧 English',
//es: '🇪🇸 Español',
//de: '🇩🇪 Deutsch'
};
return (
<div className="relative inline-block">
<select
value={currentLanguage}
onChange={(e) => onLanguageChange(e.target.value as Language)}
className="bg-banquise-blue-dark/20 border border-banquise-blue-lightest/30 text-banquise-gray rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-banquise-blue-light"
>
{availableLanguages.map((lang) => (
<option key={lang} value={lang} className="bg-banquise-blue-dark text-banquise-gray">
{languageNames[lang]}
</option>
))}
</select>
</div>
);
};

View File

@ -1,135 +0,0 @@
import React, { useEffect } from 'react';
import { URLS } from '../../config/constants';
import type { Service } from '../../types/service';
import type { Translation } from '../../types/i18n';
interface PopupProps {
service: Service;
onClose: () => void;
translations: Translation['common'];
}
export const Popup: React.FC<PopupProps> = ({ service, onClose, translations }) => {
// Empêcher le scroll du body quand la popup est ouverte
useEffect(() => {
document.body.style.overflow = 'hidden';
return () => {
document.body.style.overflow = 'unset';
};
}, []);
return (
<div className="fixed inset-0 bg-black/60 flex justify-center items-center z-50 p-4 backdrop-blur-md animate-fadeIn">
<div className="bg-white text-banquise-blue-dark rounded-3xl max-w-4xl w-full max-h-[90vh] shadow-2xl relative animate-slideUp border border-banquise-blue-lightest/20 overflow-hidden">
{/* Bouton de fermeture fixe au-dessus du contenu */}
<div className="absolute top-4 right-4 z-50">
<button
onClick={onClose}
className="bg-white/90 hover:bg-white border border-banquise-blue/20 text-xl cursor-pointer text-banquise-blue-dark 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"
aria-label={translations.close}
>
×
</button>
</div>
{/* Contenu avec scroll vertical uniquement */}
<div className="overflow-y-auto overflow-x-hidden max-h-[90vh] popup-content">
{/* Header */}
<div className="relative bg-gradient-to-r from-banquise-blue to-banquise-blue-light p-6 sm:p-8 text-white pr-16 sm:pr-20">
<div className="flex flex-col lg:flex-row items-center lg:items-start mb-4">
<div className="w-16 h-16 sm:w-20 sm:h-20 lg:w-24 lg:h-24 bg-white/20 rounded-3xl flex items-center justify-center text-3xl sm:text-4xl lg:text-5xl mb-4 lg:mb-0 lg:mr-8 backdrop-blur-sm">
{service.icon}
</div>
<div className="text-center lg:text-left flex-1">
<h2 className="font-heading text-2xl sm:text-3xl lg:text-4xl mt-0 mb-3 lg:mb-4 leading-tight font-bold text-white">
{service.name}
</h2>
<div className="text-white/90 text-base sm:text-lg lg:text-xl font-medium">
Service d'hébergement professionnel
</div>
<div className="mt-4 lg:mt-6 flex flex-wrap gap-2 justify-center lg:justify-start">
<span className="bg-white/20 text-white px-3 py-1 rounded-full text-sm font-medium backdrop-blur-sm">Haute disponibilité</span>
<span className="bg-white/20 text-white px-3 py-1 rounded-full text-sm font-medium backdrop-blur-sm">Open Source</span>
<span className="bg-white/20 text-white px-3 py-1 rounded-full text-sm font-medium backdrop-blur-sm">Communautaire</span>
</div>
</div>
</div>
</div>
{/* Content - Forcer le fond blanc */}
<div className="p-6 sm:p-8 bg-white">
{/* Description */}
<h3 className="text-xl sm:text-2xl lg:text-3xl mb-4 lg:mb-6 text-banquise-blue-dark font-heading font-bold flex items-center">
<span className="text-xl sm:text-2xl lg:text-3xl mr-3">📋</span>
Description détaillée
</h3>
<div className="bg-gradient-to-br from-banquise-blue/5 to-banquise-blue-light/5 rounded-2xl p-4 lg:p-6 border border-banquise-blue/10 mb-8">
<p className="text-banquise-blue-dark/90 leading-relaxed text-base sm:text-lg lg:text-xl mb-4">
{service.description}
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-6">
<div className="flex items-center p-3 bg-white/60 rounded-xl border border-banquise-blue/10">
<div className="w-10 h-10 bg-gradient-to-br from-banquise-blue to-banquise-blue-light rounded-lg flex items-center justify-center text-white mr-3">
</div>
<div>
<div className="font-semibold text-banquise-blue-dark text-sm">99.9% Uptime</div>
<div className="text-banquise-blue-dark/70 text-xs">Disponibilité garantie</div>
</div>
</div>
<div className="flex items-center p-3 bg-white/60 rounded-xl border border-banquise-blue/10">
<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">
🔒
</div>
<div>
<div className="font-semibold text-banquise-blue-dark text-sm">Sécurisé</div>
<div className="text-banquise-blue-dark/70 text-xs">SSL & Backups</div>
</div>
</div>
</div>
</div>
{/* Fonctionnalités */}
<h3 className="text-xl sm:text-2xl lg:text-3xl mb-4 lg:mb-6 text-banquise-blue-dark font-heading font-bold flex items-center">
<span className="text-xl sm:text-2xl lg:text-3xl mr-3"></span>
{translations.discoverFeatures}
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
{service.features.map((feature, index) => (
<div key={index} className="flex items-start bg-banquise-blue/5 rounded-xl p-4 border border-banquise-blue/10 hover:bg-banquise-blue/10 transition-colors duration-200 group">
<div className="w-6 h-6 bg-gradient-to-br from-banquise-blue to-banquise-blue-light 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-banquise-blue-dark/90 font-medium text-sm lg:text-base leading-relaxed">{feature}</span>
</div>
))}
</div>
{/* Call to action */}
<div className="pt-6 lg:pt-8 border-t border-banquise-blue/10">
<a
href={service.url}
target="_blank"
rel="noopener noreferrer"
className="w-full inline-flex items-center justify-center bg-gradient-to-r from-banquise-blue to-banquise-blue-light 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-banquise-blue-light text-base lg:text-lg hover:scale-[1.02] active:scale-95"
>
<span className="mr-3 text-xl lg:text-2xl">🚀</span>
<span>Accéder à {service.name}</span>
</a>
<p className="text-center text-sm text-banquise-blue-dark/60 mt-4">
Besoin d'aide ? Rejoignez notre <a href={URLS.social.discord} className="text-banquise-blue hover:text-banquise-blue-dark transition-colors duration-200 font-medium">Discord</a> pour obtenir du support
</p>
</div>
</div>
</div>
{/* Decorative elements */}
<div className="absolute top-0 right-0 w-16 h-16 sm:w-24 sm:h-24 lg:w-32 lg:h-32 bg-banquise-blue-lightest/10 rounded-full -translate-y-8 translate-x-8 sm:-translate-y-12 sm:translate-x-12 lg:-translate-y-16 lg:translate-x-16 hidden sm:block pointer-events-none"></div>
<div className="absolute bottom-0 left-0 w-12 h-12 sm:w-16 sm:h-16 lg:w-24 lg:h-24 bg-banquise-blue/5 rounded-full translate-y-6 -translate-x-6 sm:translate-y-8 sm:-translate-x-8 lg:translate-y-12 lg:-translate-x-12 hidden sm:block pointer-events-none"></div>
</div>
</div>
);
};

View File

@ -1,35 +0,0 @@
import React from 'react';
import { useScrollEffects } from '../../hooks/useScrollEffects';
export const ScrollToTopButton: React.FC = () => {
const { isVisible, scrollToTop } = useScrollEffects();
return (
<button
onClick={scrollToTop}
className={`fixed bottom-6 right-6 z-50 w-12 h-12 sm:w-14 sm:h-14 bg-gradient-to-r from-banquise-blue to-banquise-blue-light text-white rounded-full shadow-lg hover:shadow-xl transition-all duration-300 flex items-center justify-center group border border-banquise-blue-lightest/30 backdrop-blur-sm ${
isVisible
? 'opacity-100 translate-y-0 scale-100'
: 'opacity-0 translate-y-4 scale-95 pointer-events-none'
}`}
aria-label="Retour en haut de page"
>
<svg
className="w-5 h-5 sm:w-6 sm:h-6 transition-transform duration-300 group-hover:-translate-y-0.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2.5}
d="M7 11l5-5m0 0l5 5m-5-5v12"
/>
</svg>
{/* Effet de lueur au hover */}
<div className="absolute inset-0 bg-gradient-to-r from-banquise-blue-light to-banquise-blue rounded-full opacity-0 group-hover:opacity-75 transition-opacity duration-300 blur-sm"></div>
</button>
);
};

View File

@ -1,22 +0,0 @@
import { useState, useEffect } from 'react';
export const useOceanDepthEffect = () => {
const [scrollDepth, setScrollDepth] = useState(0);
useEffect(() => {
const handleScroll = () => {
const scrollPosition = window.scrollY;
const documentHeight = document.documentElement.scrollHeight - window.innerHeight;
const scrollPercentage = Math.min(scrollPosition / documentHeight, 1);
setScrollDepth(scrollPercentage);
};
window.addEventListener('scroll', handleScroll);
handleScroll(); // Initial call
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return scrollDepth;
};

View File

@ -1,34 +0,0 @@
import { useState, useEffect, useMemo } from 'react';
import type { Language, Translation } from '../types/i18n';
import { translations, defaultLanguage } from '../data/translations';
export const useTranslation = () => {
const [currentLanguage, setCurrentLanguage] = useState<Language>(() => {
// Récupérer la langue depuis localStorage ou utiliser la langue par défaut
const saved = localStorage.getItem('language') as Language;
return saved && translations[saved] ? saved : defaultLanguage;
});
// Memoize the translation object to prevent unnecessary re-renders
const t = useMemo<Translation>(() => translations[currentLanguage], [currentLanguage]);
// Memoize available languages array
const availableLanguages = useMemo(() => Object.keys(translations) as Language[], []);
useEffect(() => {
localStorage.setItem('language', currentLanguage);
}, [currentLanguage]);
const changeLanguage = (language: Language) => {
if (translations[language]) {
setCurrentLanguage(language);
}
};
return {
t,
currentLanguage,
changeLanguage,
availableLanguages
};
};

View File

@ -1,185 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Variables CSS pour les polices */
:root {
--font-heading: 'Dela Gothic One', sans-serif;
--font-body: 'Roboto', sans-serif;
}
/* Parallax animations */
@keyframes parallax-slow {
0% { transform: translateY(0px); }
100% { transform: translateY(-50px); }
}
@keyframes parallax-medium {
0% { transform: translateY(0px); }
100% { transform: translateY(-80px); }
}
@keyframes parallax-fast {
0% { transform: translateY(0px); }
100% { transform: translateY(-120px); }
}
@keyframes parallax-very-slow {
0% { transform: translateY(0px); }
100% { transform: translateY(-20px); }
}
/* Floating animations with different speeds */
@keyframes float-slow {
0%, 100% { transform: translateY(0px) rotate(0deg); }
50% { transform: translateY(-20px) rotate(5deg); }
}
@keyframes float-medium {
0%, 100% { transform: translateY(0px) rotate(0deg); }
50% { transform: translateY(-15px) rotate(-3deg); }
}
@keyframes float-fast {
0%, 100% { transform: translateY(0px) rotate(0deg); }
50% { transform: translateY(-10px) rotate(3deg); }
}
@keyframes float-very-slow {
0%, 100% { transform: translateY(0px) rotate(0deg); }
50% { transform: translateY(-30px) rotate(-2deg); }
}
/* Animations pour les éléments flottants */
@keyframes float-0 {
0%, 100% { transform: translateY(0px) rotate(0deg); }
50% { transform: translateY(-10px) rotate(2deg); }
}
@keyframes float-1 {
0%, 100% { transform: translateY(0px) rotate(0deg); }
33% { transform: translateY(-8px) rotate(-1deg); }
66% { transform: translateY(-15px) rotate(1deg); }
}
@keyframes float-2 {
0%, 100% { transform: translateY(0px) rotate(0deg); }
25% { transform: translateY(-12px) rotate(1deg); }
75% { transform: translateY(-6px) rotate(-2deg); }
}
/* Animation pour l'élément principal du hero */
@keyframes gentle-float {
0%, 100% { transform: translateY(0px) rotate(0deg); }
50% { transform: translateY(-15px) rotate(1deg); }
}
.animate-gentle-float {
animation: gentle-float 6s ease-in-out infinite;
}
/* Effet de lueur pour les éléments techniques */
@keyframes glow-pulse {
0%, 100% {
text-shadow: 0 0 5px rgba(168, 218, 255, 0.3),
0 0 10px rgba(168, 218, 255, 0.2),
0 0 15px rgba(168, 218, 255, 0.1);
}
50% {
text-shadow: 0 0 10px rgba(168, 218, 255, 0.4),
0 0 20px rgba(168, 218, 255, 0.3),
0 0 30px rgba(168, 218, 255, 0.2);
}
}
/* Animation des lignes de connexion */
@keyframes data-flow {
0% { stroke-dasharray: 0, 100; }
50% { stroke-dasharray: 50, 100; }
100% { stroke-dasharray: 100, 100; }
}
/* Responsive pour les animations */
@media (prefers-reduced-motion: reduce) {
.animate-gentle-float,
.animate-ping,
.animate-pulse {
animation: none;
}
}
/* Apply animations */
.animate-parallax-slow {
animation: parallax-slow 20s ease-in-out infinite;
}
.animate-parallax-medium {
animation: parallax-medium 15s ease-in-out infinite;
}
.animate-parallax-fast {
animation: parallax-fast 10s ease-in-out infinite;
}
.animate-parallax-very-slow {
animation: parallax-very-slow 30s ease-in-out infinite;
}
.animate-float-slow {
animation: float-slow 8s ease-in-out infinite;
}
.animate-float-medium {
animation: float-medium 6s ease-in-out infinite;
}
.animate-float-fast {
animation: float-fast 4s ease-in-out infinite;
}
.animate-float-very-slow {
animation: float-very-slow 12s ease-in-out infinite;
}
/* Amélioration du scroll */
html {
scroll-behavior: smooth;
}
/* Empêcher le scroll horizontal global */
body {
overflow-x: hidden;
}
/* Styles pour les popups */
.popup-content {
scrollbar-width: thin;
scrollbar-color: rgba(31, 93, 137, 0.3) transparent;
}
.popup-content::-webkit-scrollbar {
width: 6px;
}
.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);
}
/* Animation pour le bouton scroll to top */
@keyframes bounce-up {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-4px); }
}
.scroll-to-top:hover {
animation: bounce-up 0.6s ease-in-out;
}

View File

@ -1,22 +0,0 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
// Ajouter les métadonnées SEO
document.title = 'La Banquise - Hébergement et Communauté Tech';
const metaDescription = document.createElement('meta');
metaDescription.name = 'description';
metaDescription.content = 'Association d\'hébergement et lab réseau pour tous les étudiants et associations de l\'EPITA. Services Wiki, Gitea, Panel de jeux.';
document.head.appendChild(metaDescription);
const metaViewport = document.createElement('meta');
metaViewport.name = 'viewport';
metaViewport.content = 'width=device-width, initial-scale=1.0';
document.head.appendChild(metaViewport);
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

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

View File

@ -1,176 +0,0 @@
// Design System - Centralized design tokens and reusable styles
export const designTokens = {
// Colors
colors: {
banquise: {
blue: '#40B4FF',
blueDark: '#1F5D89',
blueLight: '#69B7E2',
blueLightest: '#A5F0FF',
gray: '#F6F6F6',
}
},
// Spacing
spacing: {
xs: '0.25rem',
sm: '0.5rem',
md: '1rem',
lg: '1.5rem',
xl: '2rem',
xxl: '3rem',
},
// Border radius
borderRadius: {
sm: '0.5rem',
md: '0.75rem',
lg: '1rem',
xl: '1.5rem',
xxl: '2rem',
},
// Shadows
shadows: {
sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
md: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1)',
xxl: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
},
// Typography
typography: {
fontFamily: {
heading: ['Dela Gothic One', 'sans-serif'],
body: ['Roboto', 'sans-serif'],
},
fontSize: {
xs: '0.75rem',
sm: '0.875rem',
base: '1rem',
lg: '1.125rem',
xl: '1.25rem',
'2xl': '1.5rem',
'3xl': '1.875rem',
'4xl': '2.25rem',
'5xl': '3rem',
'6xl': '3.75rem',
},
fontWeight: {
normal: '400',
medium: '500',
semibold: '600',
bold: '700',
extrabold: '800',
},
},
// Transitions
transitions: {
fast: 'all 0.15s ease-in-out',
normal: 'all 0.3s ease-in-out',
slow: 'all 0.5s ease-in-out',
},
// Z-index
zIndex: {
dropdown: 1000,
sticky: 1020,
fixed: 1030,
modalBackdrop: 1040,
modal: 1050,
popover: 1060,
tooltip: 1070,
},
} as const;
// Reusable gradient combinations
export const 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',
background: 'bg-gradient-to-b from-banquise-blue-dark via-banquise-blue-dark/95 to-banquise-blue-dark',
} as const;
// Reusable component styles
export const componentStyles = {
// Buttons
button: {
base: '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',
primary: 'bg-gradient-to-r from-banquise-blue to-banquise-blue-light',
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 bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500',
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',
},
// Cards
card: {
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',
gradient: 'bg-gradient-to-br from-banquise-blue-dark/10 to-banquise-blue-dark/5',
},
// Navigation
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',
},
// Text styles
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',
},
// Icons
icon: {
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',
},
// Layout
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',
},
// Animations
animation: {
hover: 'hover:-translate-y-4 hover:shadow-2xl transition-all duration-300',
scale: 'hover:scale-105 active:scale-95 transition-transform duration-300',
fadeIn: 'animate-fadeIn',
slideUp: 'animate-slideUp',
},
} as const;
// Utility function to merge classes
export const mergeClasses = (...classes: (string | undefined | null | false)[]): string => {
return classes.filter(Boolean).join(' ');
};
// Responsive breakpoints
export const breakpoints = {
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
'2xl': '1536px',
} as const;
export type DesignTokens = typeof designTokens;
export type Gradients = typeof gradients;
export type ComponentStyles = typeof componentStyles;

View File

@ -1,23 +0,0 @@
import type { Service } from './service';
export interface Translation {
services: Service[];
hero: {
title: string;
subtitle: string;
cta: string;
};
navigation: {
home: string;
services: string;
about: string;
contact: string;
};
common: {
discoverFeatures: string;
close: string;
loading: string;
};
}
export type Language = 'fr' | 'en'; //| 'es' | 'de';

View File

@ -1,9 +0,0 @@
// Re-export types from their specific modules
export type { Service } from './service';
export interface AccordionItemProps {
title: string;
children: React.ReactNode;
isOpen: boolean;
onToggle: () => void;
}

View File

@ -1,8 +0,0 @@
export interface Service {
name: string;
url: string;
image: string;
icon: string;
description: string;
features: string[];
}

View File

@ -1,30 +0,0 @@
/**
* Utility function to conditionally merge CSS classes
* Filters out falsy values and joins valid class names
*/
export const cn = (...classes: (string | undefined | null | false)[]): string => {
return classes.filter(Boolean).join(' ');
};
/**
* Alias for cn function for backward compatibility
*/
export const mergeClasses = cn;
/**
* Utility to create conditional classes based on state
*/
export const conditionalClass = (condition: boolean, trueClass: string, falseClass: string = ''): string => {
return condition ? trueClass : falseClass;
};
/**
* Utility to create variant-based classes
*/
export const variantClass = <T extends string>(
variant: T,
variants: Record<T, string>,
defaultVariant?: T
): string => {
return variants[variant] || (defaultVariant ? variants[defaultVariant] : '');
};

View File

@ -1,3 +0,0 @@
/// <reference types="vite/client" />
/// <reference types="react" />
/// <reference types="react-dom" />

View File

@ -1,152 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
banquise: {
blue: '#40B4FF',
'blue-dark': '#1F5D89',
'blue-light': '#69B7E2',
'blue-lightest': '#A5F0FF',
gray: '#F6F6F6',
}
},
fontFamily: {
heading: ['Dela Gothic One', 'sans-serif'],
body: ['Roboto', 'sans-serif'],
},
animation: {
'float': 'float 6s ease-in-out infinite',
'float-1': 'float1 5s ease-in-out infinite',
'float-2': 'float2 6s ease-in-out infinite',
'float-3': 'float3 7s ease-in-out infinite',
'wave': 'wave 10s linear infinite',
'wave-reverse': 'waveReverse 15s linear infinite',
'wave-1': 'wave 20s linear infinite',
'wave-2': 'waveReverse 15s linear infinite',
'wave-3': 'wave 12s linear infinite',
'rise': 'rise 10s infinite ease-in',
'tech-float': 'tech-float 10s ease-in-out infinite',
'gentle-float': 'gentle-float 6s ease-in-out infinite',
'fadeIn': 'fadeIn 0.2s ease-out',
'slideUp': 'slideUp 0.3s ease-out',
'bubble-float': 'bubble-float 8s ease-in-out infinite',
'bubble-float-slow': 'bubble-float-slow 12s ease-in-out infinite',
'bubble-float-fast': 'bubble-float-fast 6s ease-in-out infinite',
'ocean-shimmer': 'ocean-shimmer 10s ease-in-out infinite',
},
keyframes: {
float: {
'0%, 100%': { transform: 'translateY(0)' },
'50%': { transform: 'translateY(-10px)' },
},
float1: {
'0%, 100%': { transform: 'translateY(0)' },
'50%': { transform: 'translateY(-15px)' },
},
float2: {
'0%, 100%': { transform: 'translateY(0)' },
'50%': { transform: 'translateY(-20px)' },
},
float3: {
'0%, 100%': { transform: 'translateY(0)' },
'50%': { transform: 'translateY(-10px)' },
},
'tech-float': {
'0%, 100%': { transform: 'translateY(0) rotate(0deg)', opacity: '0.15' },
'50%': { transform: 'translateY(-20px) rotate(10deg)', opacity: '0.25' },
},
'gentle-float': {
'0%, 100%': { transform: 'translateY(0)' },
'50%': { transform: 'translateY(-10px)' },
},
wave: {
'0%': { backgroundPosition: '0' },
'100%': { backgroundPosition: '1200px' },
},
waveReverse: {
'0%': { backgroundPosition: '1200px' },
'100%': { backgroundPosition: '0' },
},
rise: {
'0%': {
bottom: '-100px',
transform: 'translateX(0)',
opacity: '0.8',
},
'50%': {
transform: 'translateX(40px)',
opacity: '0.4',
},
'100%': {
bottom: '1080px',
transform: 'translateX(-40px)',
opacity: '0',
},
},
fadeIn: {
from: { opacity: '0' },
to: { opacity: '1' },
},
slideUp: {
from: { transform: 'translateY(30px)', opacity: '0' },
to: { transform: 'translateY(0)', opacity: '1' },
},
'bubble-float': {
'0%': { transform: 'translateY(0) translateX(0) scale(1)', opacity: '0.6' },
'25%': { transform: 'translateY(-15px) translateX(5px) scale(1.05)', opacity: '0.7' },
'50%': { transform: 'translateY(-30px) translateX(-3px) scale(1.1)', opacity: '0.5' },
'75%': { transform: 'translateY(-45px) translateX(8px) scale(1.05)', opacity: '0.4' },
'100%': { transform: 'translateY(-60px) translateX(0) scale(1)', opacity: '0.2' },
},
'bubble-float-slow': {
'0%': { transform: 'translateY(0) translateX(0) scale(0.8)', opacity: '0.4' },
'20%': { transform: 'translateY(-20px) translateX(-8px) scale(0.9)', opacity: '0.5' },
'40%': { transform: 'translateY(-40px) translateX(6px) scale(1.1)', opacity: '0.4' },
'60%': { transform: 'translateY(-60px) translateX(-4px) scale(1.2)', opacity: '0.3' },
'80%': { transform: 'translateY(-80px) translateX(10px) scale(1.0)', opacity: '0.2' },
'100%': { transform: 'translateY(-100px) translateX(0) scale(0.8)', opacity: '0.1' },
},
'bubble-float-fast': {
'0%': { transform: 'translateY(0) translateX(0) scale(1.2)', opacity: '0.8' },
'15%': { transform: 'translateY(-10px) translateX(4px) scale(1.1)', opacity: '0.7' },
'30%': { transform: 'translateY(-20px) translateX(-2px) scale(0.9)', opacity: '0.6' },
'45%': { transform: 'translateY(-30px) translateX(6px) scale(1.0)', opacity: '0.5' },
'60%': { transform: 'translateY(-40px) translateX(-5px) scale(1.1)', opacity: '0.4' },
'75%': { transform: 'translateY(-50px) translateX(3px) scale(1.0)', opacity: '0.3' },
'90%': { transform: 'translateY(-60px) translateX(7px) scale(0.9)', opacity: '0.2' },
'100%': { transform: 'translateY(-70px) translateX(0) scale(1.2)', opacity: '0.1' },
},
'ocean-shimmer': {
'0%, 100%': { opacity: '0.1', transform: 'translateX(-10px)' },
'50%': { opacity: '0.3', transform: 'translateX(10px)' },
},
},
backdropBlur: {
'xs': '2px',
},
backgroundImage: {
'wave-pattern': "url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1200 120\" preserveAspectRatio=\"none\"><path d=\"M0,0V46.29c47.79,22.2,103.59,32.17,158,28,70.36-5.37,136.33-33.31,206.8-37.5C438.64,32.43,512.34,53.67,583,72.05c69.27,18,138.3,24.88,209.4,13.08,36.15-6,69.85-17.84,104.45-29.34C989.49,25,1113-14.29,1200,52.47V0Z\" opacity=\".25\" fill=\"%231F5D89\"/><path d=\"M0,0V15.81C13,36.92,27.64,56.86,47.69,72.05,99.41,111.27,165,111,224.58,91.58c31.15-10.15,60.09-26.07,89.67-39.8,40.92-19,84.73-46,130.83-49.67,36.26-2.85,70.9,9.42,98.6,31.56,31.77,25.39,62.32,62,103.63,73,40.44,10.79,81.35-6.69,119.13-24.28s75.16-39,116.92-43.05c59.73-5.85,113.28,22.88,168.9,38.84,30.2,8.66,59,6.17,87.09-7.5,22.43-10.89,48-26.93,60.65-49.24V0Z\" opacity=\".5\" fill=\"%231F5D89\"/><path d=\"M0,0V5.63C149.93,59,314.09,71.32,475.83,42.57c43-7.64,84.23-20.12,127.61-26.46,59-8.63,112.48,12.24,165.56,35.4C827.93,77.22,886,95.24,951.2,90c86.53-7,172.46-45.71,248.8-84.81V0Z\" fill=\"%231F5D89\"/></svg>')",
'ocean-gradient': 'linear-gradient(180deg, #40B4FF 0%, #69B7E2 50%, #1F5D89 100%)',
},
maxHeight: {
'0': '0',
'1000': '1000px',
},
spacing: {
'72': '18rem',
'80': '20rem',
'88': '22rem',
'96': '24rem',
},
},
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
],
}

View File

@ -1,27 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

View File

@ -1,7 +1,41 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
"compilerOptions": {
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": [
"./*"
]
},
"target": "ES2017"
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}

View File

@ -1,25 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,134 @@
import type { Service } from './service';
export interface Translation {
services: Service[];
hero: {
title: string;
subtitle: string;
cta: string;
};
navigation: {
home: string;
services: string;
about: string;
contact: string;
};
common: {
discoverFeatures: string;
close: string;
loading: string;
learnMore: string;
backToHome: string;
discoverOffer: string;
learnMoreAboutUs: string;
sendEmail: string;
login: string;
joinCommunity: string;
backToTop: string;
};
user: {
profile: string;
logout: string;
groups: string;
userMenu: string;
connecting: string;
authError: string;
};
about: {
title: string;
subtitle: string;
faqTitle: string;
mission: {
title: string;
description1: string;
description2: string;
tags: {
collaboration: string;
innovation: string;
accessibility: string;
};
};
services: {
title: string;
wiki: {
title: string;
description: string;
};
gitea: {
title: string;
description: string;
};
panel: {
title: string;
description: string;
};
pelican: {
title: string;
description: string;
};
intranet: {
title: string;
description: string;
};
mails: {
title: string;
description: string;
};
opencloud: {
title: string;
description: string;
};
note: string;
};
community: {
title: string;
description: string;
howToJoin: string;
steps: {
step1: string;
step2: string;
step3: string;
};
joinDiscord: string;
};
};
sections: {
ourServices: string;
};
footer: {
description: string;
ourServices: string;
community: string;
joinAssociation: string;
joinDescription: string;
joinNow: string;
gamingPanel: string;
madeWith: string;
by: string;
copyright: string;
};
infrastructure: {
title: string;
subtitle: string;
features: {
performance: {
title: string;
description: string;
};
storage: {
title: string;
description: string;
};
network: {
title: string;
description: string;
};
security: {
title: string;
description: string;
};
};
};
}
export type Language = 'fr' | 'en'; //| 'es' | 'de';

View File

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

Some files were not shown because too many files have changed in this diff Show More