Compare commits
17 Commits
main
...
major-upda
Author | SHA1 | Date | |
---|---|---|---|
81e63e5a8d | |||
796c7d1c21 | |||
0b6960085d | |||
df15c7a838 | |||
7e47c6163d | |||
57f5807876 | |||
8b374cf8c4 | |||
54c51341a2 | |||
54d419d17a | |||
ff63b5958a | |||
a63d9f4797 | |||
30fd66f2c9 | |||
d36f6f48e8 | |||
b32efb439c | |||
e1017e7570 | |||
545b7f9d91 | |||
537c2b01d1 |
@ -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
@ -8,6 +8,7 @@
|
||||
# dotenv environment files (si tu en as)
|
||||
.env
|
||||
.env.*.local
|
||||
.next
|
||||
|
||||
# system files
|
||||
.DS_Store
|
||||
|
85
README.md
@ -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
|
||||
```
|
243
banquise-website/.gitignore
vendored
@ -1,3 +1,159 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
.nyc_output
|
||||
|
||||
# Next.js
|
||||
.next/
|
||||
out/
|
||||
build/
|
||||
dist/
|
||||
|
||||
# Production builds
|
||||
*.tgz
|
||||
*.tar.gz
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
public
|
||||
|
||||
# Vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS)
|
||||
.DS_Store
|
||||
|
||||
# Windows
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
|
||||
# VS Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
# ESLint cache
|
||||
.eslintcache
|
||||
|
||||
# Prettier cache
|
||||
.prettiercache
|
||||
|
||||
# Stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.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
|
||||
|
38
banquise-website/Dockerfile
Normal 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"]
|
@ -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,
|
||||
},
|
||||
})
|
||||
```
|
126
banquise-website/app/globals.css
Normal 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);
|
||||
}
|
18
banquise-website/app/layout.tsx
Normal 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>
|
||||
)
|
||||
}
|
108
banquise-website/app/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
npm install
|
||||
npm run build
|
74
banquise-website/components/common/Button.tsx
Normal 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>
|
||||
);
|
||||
};
|
114
banquise-website/components/common/ServiceCard.tsx
Normal 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>
|
||||
);
|
||||
};
|
177
banquise-website/components/common/UserProfile.tsx
Normal 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>
|
||||
);
|
||||
};
|
151
banquise-website/components/layout/Footer.tsx
Normal 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>
|
||||
);
|
||||
};
|
160
banquise-website/components/layout/MobileMenu.tsx
Normal 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>
|
||||
);
|
||||
};
|
73
banquise-website/components/layout/ModernNavigation.tsx
Normal 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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
28
banquise-website/components/layout/navbar/ActionButtons.tsx
Normal 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>
|
||||
);
|
||||
};
|
64
banquise-website/components/layout/navbar/Logo.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
144
banquise-website/components/layout/navbar/NavLinks.tsx
Normal 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>
|
||||
);
|
||||
};
|
171
banquise-website/components/sections/AboutSection.tsx
Normal 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>
|
||||
);
|
||||
};
|
148
banquise-website/components/sections/HeroSection.tsx
Normal 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>
|
||||
);
|
47
banquise-website/components/sections/ServicesSection.tsx
Normal 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>
|
||||
);
|
||||
};
|
89
banquise-website/components/sections/TechFeaturesSection.tsx
Normal 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>
|
||||
);
|
||||
};
|
24
banquise-website/components/ui/AccordionItem.tsx
Normal 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>
|
||||
);
|
17
banquise-website/components/ui/AccordionTitle.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface AccordionTitleProps {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const AccordionTitle: React.FC<AccordionTitleProps> = ({ icon: Icon, title }) => (
|
||||
<div className="flex items-center">
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center text-white mr-3">
|
||||
<Icon className="w-5 h-5" strokeWidth={2} />
|
||||
</div>
|
||||
{title}
|
||||
</div>
|
||||
);
|
47
banquise-website/components/ui/DiscordButton.tsx
Normal 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;
|
62
banquise-website/components/ui/DiscordLogo.tsx
Normal 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;
|
@ -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>
|
||||
);
|
88
banquise-website/components/ui/OceanBackground.tsx
Normal 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>
|
||||
);
|
||||
};
|
149
banquise-website/components/ui/Popup.tsx
Normal 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'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'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>
|
||||
);
|
||||
};
|
53
banquise-website/components/ui/PopupComponents.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface FeatureBadgeProps {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
}
|
||||
|
||||
export const FeatureBadge: React.FC<FeatureBadgeProps> = ({
|
||||
icon: Icon,
|
||||
title,
|
||||
subtitle
|
||||
}) => (
|
||||
<div className="flex items-center p-3 bg-white/80 rounded-xl border border-gray-200 shadow-sm">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-green-500 to-green-600 rounded-lg flex items-center justify-center text-white mr-3">
|
||||
<Icon className="w-5 h-5" strokeWidth={2} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold text-gray-800 text-sm">{title}</div>
|
||||
<div className="text-gray-600 text-xs">{subtitle}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
interface FeatureItemProps {
|
||||
feature: string;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export const FeatureItem: React.FC<FeatureItemProps> = ({ feature, index }) => (
|
||||
<div className="flex items-start bg-blue-50 rounded-xl p-4 border border-blue-200 hover:bg-blue-100 transition-colors duration-200 group">
|
||||
<div className="w-6 h-6 bg-gradient-to-br from-blue-600 to-blue-500 rounded-full flex items-center justify-center mr-3 mt-0.5 flex-shrink-0 group-hover:scale-110 transition-transform duration-200">
|
||||
<div className="w-2 h-2 bg-white rounded-full"></div>
|
||||
</div>
|
||||
<span className="text-gray-700 font-medium text-sm lg:text-base leading-relaxed">{feature}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
interface SectionTitleProps {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const SectionTitle: React.FC<SectionTitleProps> = ({ icon: Icon, title }) => (
|
||||
<h3 className="text-xl sm:text-2xl lg:text-3xl mb-4 lg:mb-6 text-gray-800 font-heading font-bold flex items-center">
|
||||
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center text-white mr-3">
|
||||
<Icon className="w-5 h-5 sm:w-6 sm:h-6" strokeWidth={2} />
|
||||
</div>
|
||||
{title}
|
||||
</h3>
|
||||
);
|
29
banquise-website/components/ui/ScrollToTopButton.tsx
Normal 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>
|
||||
);
|
||||
};
|
31
banquise-website/components/ui/SectionHeader.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface SectionHeaderProps {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const SectionHeader: React.FC<SectionHeaderProps> = ({
|
||||
title,
|
||||
subtitle,
|
||||
className
|
||||
}) => (
|
||||
<div className={cn('text-center mb-20', className)}>
|
||||
{/* Séparateur visuel moderne - factorisation du design répétitif */}
|
||||
<div className="w-24 h-1.5 bg-gradient-to-r from-blue-600 to-blue-400 rounded-full mx-auto mb-8" />
|
||||
|
||||
{/* Titre principal avec contraste fort */}
|
||||
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-gray-900 mb-6 leading-tight">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
{/* Sous-titre avec bon contraste */}
|
||||
{subtitle && (
|
||||
<p className="text-lg md:text-xl text-gray-700 mx-auto max-w-3xl leading-relaxed font-medium">
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
33
banquise-website/components/ui/ServiceCardAbout.tsx
Normal file
@ -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>
|
||||
);
|
@ -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 },
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
@ -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>
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
@ -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,
|
@ -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);
|
55
banquise-website/lib/hooks/useAuth.ts
Normal 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,
|
||||
};
|
||||
};
|
118
banquise-website/lib/hooks/useCommon.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import { useEffect, useCallback, useState } from 'react';
|
||||
|
||||
/**
|
||||
* Hook centralisé pour la gestion du scroll du body
|
||||
* Factorisation de la logique répétée dans plusieurs composants
|
||||
*/
|
||||
export const useBodyScrollLock = (isLocked: boolean) => {
|
||||
useEffect(() => {
|
||||
const originalStyle = document.body.style.overflow;
|
||||
|
||||
if (isLocked) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = originalStyle || 'unset';
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = originalStyle || 'unset';
|
||||
};
|
||||
}, [isLocked]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook pour gérer les événements de redimensionnement avec debounce
|
||||
*/
|
||||
export const useResizeEvent = (callback: () => void, breakpoint: number = 768, delay: number = 100) => {
|
||||
useEffect(() => {
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
|
||||
const handleResize = () => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => {
|
||||
if (window.innerWidth >= breakpoint) {
|
||||
callback();
|
||||
}
|
||||
}, delay);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [callback, breakpoint, delay]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook pour les états de toggle simples (booléens)
|
||||
*/
|
||||
export const useToggle = (initialState: boolean = false) => {
|
||||
const [state, setState] = useState(initialState);
|
||||
|
||||
const toggle = useCallback(() => setState(prev => !prev), []);
|
||||
const setTrue = useCallback(() => setState(true), []);
|
||||
const setFalse = useCallback(() => setState(false), []);
|
||||
|
||||
return {
|
||||
state,
|
||||
toggle,
|
||||
setTrue,
|
||||
setFalse,
|
||||
setState
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook pour gérer les modales génériques
|
||||
*/
|
||||
export const useModal = <T = any>(initialState: T | null = null) => {
|
||||
const [data, setData] = useState<T | null>(initialState);
|
||||
|
||||
const open = useCallback((newData: T) => {
|
||||
setData(newData);
|
||||
}, []);
|
||||
|
||||
const close = useCallback(() => {
|
||||
setData(null);
|
||||
}, []);
|
||||
|
||||
const isOpen = data !== null;
|
||||
|
||||
return {
|
||||
data,
|
||||
open,
|
||||
close,
|
||||
isOpen
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook pour la navigation entre sections avec scroll smooth
|
||||
*/
|
||||
export const useNavigation = () => {
|
||||
const scrollToSection = useCallback((sectionId: string) => {
|
||||
if (sectionId === 'home') {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
} else if (sectionId === 'contact') {
|
||||
window.location.href = 'mailto:contact@la-banquise.fr';
|
||||
} else {
|
||||
const element = document.getElementById(sectionId);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const createNavHandler = useCallback((onClose?: () => void) => {
|
||||
return (sectionId: string) => {
|
||||
scrollToSection(sectionId);
|
||||
onClose?.();
|
||||
};
|
||||
}, [scrollToSection]);
|
||||
|
||||
return {
|
||||
scrollToSection,
|
||||
createNavHandler
|
||||
};
|
||||
};
|
35
banquise-website/lib/hooks/useOceanDepthEffect.ts
Normal 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;
|
||||
};
|
@ -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
|
50
banquise-website/lib/hooks/useTranslation.tsx
Normal 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,
|
||||
};
|
||||
};
|
57
banquise-website/lib/services/auth.ts
Normal 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();
|
197
banquise-website/lib/styles/designSystem.ts
Normal file
@ -0,0 +1,197 @@
|
||||
// Design System La Banquise - Système centralisé de styles
|
||||
export const designSystem = {
|
||||
// 🎨 Gradients
|
||||
gradients: {
|
||||
primary: "bg-gradient-to-r from-banquise-blue to-banquise-blue-light",
|
||||
primaryBr: "bg-gradient-to-br from-banquise-blue to-banquise-blue-light",
|
||||
card: "bg-gradient-to-br from-banquise-blue-dark/10 to-banquise-blue-dark/5",
|
||||
cardHover: "hover:from-banquise-blue-dark/15 hover:to-banquise-blue-dark/8",
|
||||
discord: "bg-gradient-to-r from-[#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;
|
79
banquise-website/lib/utils/index.ts
Normal 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
@ -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.
|
5060
banquise-website/package-lock.json
generated
@ -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
@ -1,6 +1,7 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
'@tailwindcss/postcss': {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 115 KiB |
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 188 KiB |
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 121 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
@ -1 +0,0 @@
|
||||
/* This file is no longer needed - all styles have been converted to Tailwind CSS */
|
@ -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;
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
@ -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>
|
||||
);
|
||||
};
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
@ -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>
|
||||
);
|
@ -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>
|
||||
);
|
@ -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>
|
||||
);
|
@ -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>
|
||||
);
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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;
|
||||
};
|
@ -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
|
||||
};
|
||||
};
|
@ -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;
|
||||
}
|
@ -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>,
|
||||
)
|
@ -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;
|
@ -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;
|
@ -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';
|
@ -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;
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
export interface Service {
|
||||
name: string;
|
||||
url: string;
|
||||
image: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
features: string[];
|
||||
}
|
@ -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] : '');
|
||||
};
|
3
banquise-website/src/vite-env.d.ts
vendored
@ -1,3 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="react" />
|
||||
/// <reference types="react-dom" />
|
@ -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'),
|
||||
],
|
||||
}
|
@ -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"]
|
||||
}
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
@ -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"]
|
||||
}
|
134
banquise-website/types/i18n.ts
Normal 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';
|
48
banquise-website/types/index.ts
Normal 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';
|