Compare commits

...

28 Commits

Author SHA1 Message Date
sahamone
bd42196f54 refactoring & UI improvment
Some checks failed
Build / build-check (pull_request) Failing after 26s
2025-07-10 18:35:45 +02:00
d3ec4524a7 Merge branch 'dev' into arthur.wambst/content-refactoring
All checks were successful
Build / build-check (pull_request) Successful in 51s
2025-06-21 18:11:14 +02:00
5e42042077 Merge pull request 'content-update' (#26) from content-update into dev
All checks were successful
Build / build-check (push) Successful in 54s
Reviewed-on: #26
Reviewed-by: malopieds <malopieds@la-banquise.fr>
2025-06-21 18:08:16 +02:00
Arthur Wambst
5b354d15a6 fix: changed on push and action name
All checks were successful
Build / build-check (pull_request) Successful in 2m59s
2025-06-18 19:21:37 +02:00
Arthur Wambst
3d0efdb15c fix: discord link to count website joins
All checks were successful
Build / Explore-Gitea-Actions (push) Successful in 2m11s
2025-06-18 19:20:05 +02:00
Arthur Wambst
81dad2b6ba wip: started to add translations
All checks were successful
Build / Explore-Gitea-Actions (push) Successful in 35s
2025-06-18 04:01:37 +02:00
Arthur Wambst
34531fd2cf init
Some checks failed
Build / Explore-Gitea-Actions (push) Failing after 21s
2025-06-18 03:37:04 +02:00
Arthur Wambst
b14490da1b fix: updated discord link
All checks were successful
Build / Explore-Gitea-Actions (push) Successful in 23s
2025-06-18 03:05:18 +02:00
Arthur Wambst
4ec4d6ccb2 fix: ssp desc
All checks were successful
Build / Explore-Gitea-Actions (push) Successful in 24s
2025-06-18 02:12:29 +02:00
Arthur Wambst
0f2d0fdb40 merged
All checks were successful
Build / Explore-Gitea-Actions (push) Successful in 39s
2025-06-18 01:37:34 +02:00
Arthur Wambst
ff5984a727 fix: now it build, not sure why 2025-06-18 01:29:00 +02:00
Arthur Wambst
978e46d0a7 fix: added missing , 2025-06-18 00:59:17 +02:00
Arthur Wambst
a563a04c65 feat: added actions tests
Some checks failed
Build / Explore-Gitea-Actions (push) Failing after 21s
2025-06-11 16:21:51 +02:00
Arthur Wambst
a6d054a09a test in cd
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 59s
2025-06-11 16:14:51 +02:00
Arthur Wambst
04d87e6113 test in cd 2025-06-11 16:12:58 +02:00
Arthur Wambst
11c26e1ba9 test in cd
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s
2025-06-11 16:10:03 +02:00
Arthur Wambst
e327231b8a test in cd
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 44s
2025-06-11 16:07:31 +02:00
Arthur Wambst
cd6ef409f1 test in cd
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s
2025-06-11 16:02:58 +02:00
Arthur Wambst
80ee14b7da test in cd
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 59s
2025-06-11 15:59:08 +02:00
Arthur Wambst
870c9ac0d8 test in cd
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 1m24s
2025-06-11 15:55:16 +02:00
Arthur Wambst
bdf1124cf6 test in cd
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 10s
2025-06-11 15:53:45 +02:00
Arthur Wambst
ed775ee3e5 fix: forgot npm install
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s
2025-06-11 15:39:59 +02:00
Arthur Wambst
549a95e797 fix: wrong path in cd
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 10s
2025-06-11 15:33:59 +02:00
Arthur Wambst
a5268d3714 fix: wrong actions file path
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 10s
2025-06-11 15:32:59 +02:00
Arthur Wambst
88d7694647 tests actions 2025-06-11 15:29:19 +02:00
976e7af488 Merge pull request 'Add new links' (#25) from content-update into dev
Reviewed-on: #25
2025-06-11 10:03:33 +02:00
Arthur Wambst
6bebedb515 changed some content 2025-06-11 10:01:45 +02:00
sahamone
65ea4ac0be Add new links
Ajout de nouveaux services au site web
2025-06-10 20:03:41 +02:00
42 changed files with 7183 additions and 539 deletions

View File

@ -0,0 +1,31 @@
name: Build
run-name: CI/CD website
on:
push:
branches:
- main
- dev
pull_request:
types: [opened, synchronize, reopened]
branches:
- main
- dev
jobs:
build-check:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '24.x'
- name: Install dependencies
run: |
cd banquise-website
npm ci
- name: Building
run: |
cd banquise-website
npm run build

4
.gitignore vendored
View File

@ -30,7 +30,3 @@ pnpm-debug.log*
# optional: nix store link if using nix develop # optional: nix store link if using nix develop
.result .result
# optional: lockfiles you don't use
package-lock.json

5060
banquise-website/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,103 +1,144 @@
import React, { useState } from 'react'; import React from 'react';
import { Navigation } from './components/layout/Navigation'; import { ModernNavigation } from './components/layout/ModernNavigation';
import { Footer } from './components/layout/Footer';
import { HeroSection } from './components/sections/HeroSection'; import { HeroSection } from './components/sections/HeroSection';
import { TechFeaturesSection } from './components/sections/TechFeaturesSection';
import { ServicesSection } from './components/sections/ServicesSection'; import { ServicesSection } from './components/sections/ServicesSection';
import { TechFeaturesSection } from './components/sections/TechFeaturesSection';
import { AboutSection } from './components/sections/AboutSection'; import { AboutSection } from './components/sections/AboutSection';
import { Footer } from './components/layout/Footer';
import { Popup } from './components/ui/Popup'; import { Popup } from './components/ui/Popup';
import { ScrollToTopButton } from './components/ui/ScrollToTopButton'; import { ScrollToTopButton } from './components/ui/ScrollToTopButton';
import { URLS } from './config/constants'; import { ModernLanguageSwitcher } from './components/ui/ModernLanguageSwitcher';
import { useTranslation } from './hooks/useTranslation';
// Define Service interface directly in App import { useServiceModal } from './hooks/useServiceModal';
interface Service { import { useAccordion } from './hooks/useAccordion';
name: string; import { useOceanDepthEffect } from './hooks/useOceanDepthEffect';
url: string;
image: string;
description: string;
features: string[];
icon: string;
}
const App: React.FC = () => { const App: React.FC = () => {
// Define services directly in the component with enhanced data const { t, currentLanguage, changeLanguage, availableLanguages } = useTranslation();
const services: Service[] = [ const { selectedService, openServiceModal, closeServiceModal } = useServiceModal();
{ const { openAccordion, toggleAccordion } = useAccordion();
name: "Wiki", const scrollDepth = useOceanDepthEffect();
url: URLS.services.wiki,
image: "/src/assets/iceberg.png",
icon: "📚",
description: "Notre wiki collaboratif est votre centre de documentation technique. Accédez à des guides détaillés, des tutoriels et de la documentation API pour tous nos services.",
features: [
"Documentation collaborative en temps réel",
"Guides d'installation détaillés",
"API et références techniques",
"Tutoriels pas à pas",
"Base de connaissances communautaire",
"Recherche avancée intégrée"
]
},
{
name: "Gitea",
url: URLS.services.gitea,
image: "/src/assets/iceberg.png",
icon: "🔧",
description: "Instance Gitea auto-hébergée pour la gestion de vos dépôts Git. Collaborez sur vos projets avec un contrôle total sur votre code source.",
features: [
"Dépôts Git illimités",
"Issues et pull requests",
"Actions CI/CD intégrées",
"Gestion d'équipes et permissions",
"Interface web intuitive",
"Intégration avec outils externes"
]
},
{
name: "Panel",
url: URLS.services.panel,
image: "/src/assets/iceberg.png",
icon: "🎮",
description: "Interface de gestion centralisée pour vos serveurs de jeux. Déployez, configurez et surveillez vos serveurs gaming en quelques clics.",
features: [
"Déploiement automatisé de serveurs",
"Monitoring en temps réel",
"Gestion des ressources système",
"Interface d'administration web",
"Support multi-jeux",
"Sauvegarde automatique des données"
]
}
];
const [selectedService, setSelectedService] = useState<Service | null>(null);
// Inline accordion logic
const [openAccordion, setOpenAccordion] = useState<string | null>(null);
const toggleAccordion = (title: string) => {
setOpenAccordion(openAccordion === title ? null : title);
};
return ( return (
<div className="flex flex-col min-h-screen w-full"> <div className="min-h-screen relative overflow-x-hidden">
<Navigation /> {/* 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>
<main className="flex-1 flex flex-col overflow-x-hidden overflow-y-auto"> {/* Couche de profondeur progressive basée sur le scroll - Plus intense */}
<div className="relative flex-1 bg-ocean-gradient w-full min-h-screen flex flex-col justify-start items-center overflow-x-hidden"> <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>
<HeroSection /> {/* Effet de profondeur supplémentaire pour les moyennes profondeurs - Plus sombre */}
<TechFeaturesSection /> <div
<ServicesSection services={services} onServiceClick={setSelectedService} /> className="absolute inset-0 bg-gradient-to-b from-transparent via-banquise-blue-dark/70 to-banquise-blue-dark transition-opacity duration-500"
<AboutSection openAccordion={openAccordion} toggleAccordion={toggleAccordion} /> 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> </div>
</main>
{/* 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 /> <Footer />
</div>
{/* Bouton de retour en haut */} {/* UI Components */}
<ScrollToTopButton /> <ScrollToTopButton />
{selectedService && ( {selectedService && (
<Popup service={selectedService} onClose={() => setSelectedService(null)} /> <Popup
service={selectedService}
onClose={closeServiceModal}
translations={t.common}
/>
)} )}
</div> </div>
); );

View File

@ -0,0 +1,65 @@
import React from 'react';
import { componentStyles, mergeClasses } from '../../styles/designSystem';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'discord' | 'auth' | 'secondary';
size?: 'sm' | 'md' | 'lg';
fullWidth?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
loading?: boolean;
children: React.ReactNode;
}
const sizeClasses = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 lg:px-6 py-2.5 lg:py-3 text-sm lg:text-base',
lg: 'px-6 py-3 text-base lg:text-lg',
};
const variantClasses = {
primary: 'bg-gradient-to-r from-banquise-blue to-banquise-blue-light hover:shadow-banquise-blue/25',
discord: 'bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500 hover:shadow-indigo-500/25',
auth: 'bg-gradient-to-r from-banquise-blue-light to-banquise-blue hover:shadow-banquise-blue-light/25',
secondary: 'bg-white/10 hover:bg-white/20 border border-white/20',
};
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
fullWidth = false,
leftIcon,
rightIcon,
loading = false,
children,
className = '',
disabled,
...props
}) => {
const baseClasses = mergeClasses(
componentStyles.button.base,
sizeClasses[size],
variantClasses[variant],
fullWidth ? 'w-full' : '',
(disabled || loading) ? 'opacity-50 cursor-not-allowed' : '',
className
);
return (
<button
className={baseClasses}
disabled={disabled || loading}
{...props}
>
{loading && (
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
)}
{leftIcon && !loading && <span className="mr-2">{leftIcon}</span>}
{children}
{rightIcon && !loading && <span className="ml-2">{rightIcon}</span>}
</button>
);
};

View File

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

View File

@ -0,0 +1,57 @@
import React from 'react';
import { componentStyles, mergeClasses } from '../../styles/designSystem';
import type { Service } from '../../types/service';
interface ServiceCardProps {
service: Service;
onServiceClick: (service: Service) => void;
discoverFeaturesText: string;
className?: string;
}
export const ServiceCard: React.FC<ServiceCardProps> = ({
service,
onServiceClick,
discoverFeaturesText,
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">
{service.icon}
</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 mb-6 sm:mb-8 text-center text-sm sm:text-base">
{service.description.split('.')[0]}.
</p>
{/* CTA */}
<div className="flex items-center justify-center text-banquise-blue-light font-bold group-hover:text-banquise-blue-lightest transition-colors duration-300 text-sm sm:text-base">
<span className="text-center">{discoverFeaturesText}</span>
<span className="ml-2 text-lg transition-transform duration-300 group-hover:translate-x-2"></span>
</div>
{/* Hover effect */}
<div className="absolute inset-0 bg-gradient-to-br from-banquise-blue-lightest/10 to-banquise-blue/5 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none"></div>
</div>
);
};

View File

@ -2,65 +2,78 @@ import React from 'react';
import { URLS, SITE_CONFIG } from '../../config/constants'; import { URLS, SITE_CONFIG } from '../../config/constants';
export const Footer: React.FC = () => ( export const Footer: React.FC = () => (
<footer className="bg-banquise-blue-dark text-white py-12 sm:py-16 md:py-20 px-4 sm:px-6 md:px-8 relative z-10 border-t border-banquise-blue-lightest/20 w-full box-border"> <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="flex flex-col md:flex-row justify-between max-w-6xl mx-auto gap-6 sm:gap-8"> <div className="max-w-6xl mx-auto">
<div className="flex-1 min-w-0 mb-6 sm:mb-8 md:mb-0 text-left"> {/* Main Footer Content */}
<h4 className="text-lg sm:text-xl mb-6 sm:mb-8 text-banquise-blue-lightest relative pb-3 sm:pb-4 after:content-[''] after:absolute after:bottom-0 after:left-0 after:w-12 after:h-0.5 after:bg-gradient-to-r after:from-banquise-blue-lightest after:to-banquise-blue"> <div className="flex flex-col md:flex-row justify-between items-center gap-6 mb-6">
Services {/* Logo/Brand */}
</h4> <div className="flex items-center gap-3">
<ul className="list-none p-0 m-0 space-y-2 sm:space-y-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">
<li> <span className="text-white font-bold text-sm">B</span>
<a href={URLS.services.wiki} className="text-banquise-gray/80 no-underline transition-all duration-200 inline-flex items-center hover:text-banquise-gray hover:translate-x-1 text-sm sm:text-base"> </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 Wiki
</a> </a>
</li> <a
<li> href={URLS.services.gitea}
<a href={URLS.services.gitea} className="text-banquise-gray/80 no-underline transition-all duration-200 inline-flex items-center hover:text-banquise-gray hover:translate-x-1 text-sm sm:text-base"> className="text-banquise-gray/80 hover:text-banquise-blue-lightest transition-colors duration-200"
>
Gitea Gitea
</a> </a>
</li> <a
<li> href={URLS.services.panel}
<a href={URLS.services.panel} className="text-banquise-gray/80 no-underline transition-all duration-200 inline-flex items-center hover:text-banquise-gray hover:translate-x-1 text-sm sm:text-base"> className="text-banquise-gray/80 hover:text-banquise-blue-lightest transition-colors duration-200"
>
Panel Panel
</a> </a>
</li> <a
</ul> href={URLS.services.opencloud}
className="text-banquise-gray/80 hover:text-banquise-blue-lightest transition-colors duration-200"
>
OpenCloud
</a>
</div> </div>
<div className="flex-1 min-w-0 mb-6 sm:mb-8 md:mb-0 text-left"> {/* Social Links */}
<h4 className="text-lg sm:text-xl mb-6 sm:mb-8 text-banquise-blue-lightest relative pb-3 sm:pb-4 after:content-[''] after:absolute after:bottom-0 after:left-0 after:w-12 after:h-0.5 after:bg-gradient-to-r after:from-banquise-blue-lightest after:to-banquise-blue"> <div className="flex items-center gap-4">
Communauté <a
</h4> href={URLS.social.discord}
<ul className="list-none p-0 m-0 space-y-2 sm:space-y-3"> 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"
<li> aria-label="Discord"
<a href={URLS.social.discord} className="text-banquise-gray/80 no-underline transition-all duration-200 inline-flex items-center hover:text-banquise-gray hover:translate-x-1 text-sm sm:text-base"> >
Discord <span className="text-banquise-blue-lightest text-sm">💬</span>
</a> </a>
</li> <a
</ul> href={URLS.contact.email}
</div> 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"
<div className="flex-1 min-w-0 mb-6 sm:mb-8 md:mb-0 text-left"> >
<h4 className="text-lg sm:text-xl mb-6 sm:mb-8 text-banquise-blue-lightest relative pb-3 sm:pb-4 after:content-[''] after:absolute after:bottom-0 after:left-0 after:w-12 after:h-0.5 after:bg-gradient-to-r after:from-banquise-blue-lightest after:to-banquise-blue"> <span className="text-banquise-blue-lightest text-sm">📧</span>
Support
</h4>
<ul className="list-none p-0 m-0 space-y-2 sm:space-y-3">
<li>
<a href="#" className="text-banquise-gray/80 no-underline transition-all duration-200 inline-flex items-center hover:text-banquise-gray hover:translate-x-1 text-sm sm:text-base">
Documentation
</a> </a>
</li>
<li>
<a href={URLS.contact.email} className="text-banquise-gray/80 no-underline transition-all duration-200 inline-flex items-center hover:text-banquise-gray hover:translate-x-1 text-sm sm:text-base">
Contact
</a>
</li>
</ul>
</div> </div>
</div> </div>
<div className="border-t border-banquise-blue-lightest/20 pt-6 sm:pt-8 mt-8 sm:mt-12 text-center text-xs sm:text-sm text-banquise-gray/70 max-w-6xl mx-auto"> {/* Bottom Bar */}
© 2024 {SITE_CONFIG.name}. Tous droits réservés. <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">
© 2024 {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 la communauté</span>
<div className="w-1 h-1 bg-banquise-gray/40 rounded-full"></div>
<span>EPITA 2024</span>
</div>
</div>
</div> </div>
</footer> </footer>
); );

View File

@ -1,14 +1,89 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import banquiseServer from '../../assets/banquise_server.svg' import { Button } from '../common/Button';
import { URLS, SITE_CONFIG } from '../../config/constants'; import { mergeClasses as cn } from '../../styles/designSystem';
import { commonStyles } from '../../styles/components'; import { Logo } from './navbar/Logo';
import { URLS } from '../../config/constants';
import type { Translation } from '../../types/i18n';
interface MobileMenuProps { interface MobileMenuProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
translations: Translation['navigation'];
} }
export const MobileMenu: React.FC<MobileMenuProps> = ({ isOpen, onClose }) => { 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(() => { useEffect(() => {
if (isOpen) { if (isOpen) {
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
@ -21,133 +96,171 @@ export const MobileMenu: React.FC<MobileMenuProps> = ({ isOpen, onClose }) => {
}; };
}, [isOpen]); }, [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 ( return (
<div className={`md:hidden fixed inset-0 z-[100] transition-all duration-300 ${isOpen ? 'visible' : 'invisible'}`}> <div className={cn(
{/* Overlay */} 'md:hidden fixed inset-0 z-[100] transition-all duration-300',
isOpen ? 'visible' : 'invisible'
)}>
{/* Overlay avec effet de blur moderne */}
<div <div
className={`absolute inset-0 bg-gradient-to-br from-black/70 via-banquise-blue-dark/50 to-black/70 backdrop-blur-md transition-opacity duration-300 ${isOpen ? 'opacity-100' : 'opacity-0'}`} 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} onClick={onClose}
/> />
{/* Menu mobile */} {/* Menu Panel */}
<div className={`absolute top-0 right-0 h-full w-72 max-w-[85vw] bg-gradient-to-b from-banquise-blue-dark via-banquise-blue-dark/98 to-banquise-blue-dark/95 backdrop-blur-2xl shadow-2xl transition-transform duration-300 border-l border-banquise-blue-lightest/20 ${isOpen ? 'translate-x-0' : 'translate-x-full'}`}> <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 */} {/* Header avec Logo */}
<div className="flex items-center justify-between p-4 sm:p-6 pt-6 sm:pt-8 border-b border-banquise-blue-lightest/20 bg-gradient-to-r from-banquise-blue-dark/50 to-transparent"> <div className="flex items-center justify-between p-6 pt-8 border-b border-banquise-blue-lightest/20">
<div className="flex items-center space-x-3"> <Logo scrolled={false} />
<div className="relative">
<div className="absolute inset-0 bg-banquise-blue-light/20 rounded-full blur-md"></div>
<img
src={banquiseServer}
alt="Logo"
className="h-8 sm:h-10 w-auto relative z-10"
style={{ filter: 'drop-shadow(0 0 8px rgba(168, 218, 255, 0.6))' }}
/>
</div>
<div>
<span className={`text-base sm:text-lg font-bold text-white ${commonStyles.text.heading}`}>
{SITE_CONFIG.name}
</span>
<p className="text-banquise-blue-lightest/70 text-xs">Menu Navigation</p>
</div>
</div>
<button <button
className="group relative p-3 bg-white/10 hover:bg-white/20 rounded-xl transition-all duration-200 hover:scale-105 active:scale-95" 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} onClick={onClose}
aria-label="Fermer" aria-label="Fermer le menu"
> >
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" className="text-white"> <svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path d="M15 5L5 15M5 5L15 15" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
</button> </button>
</div> </div>
{/* Navigation */} {/* Navigation Items */}
<div className="flex flex-col justify-start px-6 py-8 space-y-6 overflow-y-auto" style={{ height: 'calc(100vh - 120px)' }}> <div className="flex flex-col h-full overflow-y-auto p-6 space-y-4">
{/* Navigation Links */} {/* Section Navigation */}
<div className="space-y-3"> <div className="space-y-3">
<a href="#services" className={commonStyles.nav.mobileItem} onClick={onClose}> <MobileNavItem
<div className="flex items-center space-x-4"> icon={
<div className={`${commonStyles.icons.small} ${commonStyles.gradients.primaryBr} group-hover:scale-110 transition-transform duration-200`}> <svg className="w-5 h-5 text-banquise-blue-lightest" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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="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" /> <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> </svg>
</div> }
<div> title={translations.services}
<span className="font-semibold text-lg">Nos Services</span> description="Découvrir notre offre"
<p className="text-white/60 text-sm">Découvrir notre offre</p> href="#services"
</div> onClick={() => handleNavClick('services')}
</div> />
<svg className="w-5 h-5 ml-auto opacity-50 group-hover:opacity-100 group-hover:translate-x-1 transition-all duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</a>
<a href="#about" className={commonStyles.nav.mobileItem} onClick={onClose}> <MobileNavItem
<div className="flex items-center space-x-4"> icon={
<div className={`${commonStyles.icons.small} ${commonStyles.gradients.primaryBr} group-hover:scale-110 transition-transform duration-200`}> <svg className="w-5 h-5 text-banquise-blue-lightest" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <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> </svg>
</div> }
<div> title={translations.about}
<span className="font-semibold text-lg">À propos</span> description="En savoir plus sur nous"
<p className="text-white/60 text-sm">En savoir plus sur nous</p> href="#about"
</div> onClick={() => handleNavClick('about')}
</div> />
<svg className="w-5 h-5 ml-auto opacity-50 group-hover:opacity-100 group-hover:translate-x-1 transition-all duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</a>
<a href={URLS.social.discord} className={commonStyles.nav.mobileItem} onClick={onClose}> <MobileNavItem
<div className="flex items-center space-x-4"> icon={
<div className={`${commonStyles.icons.small} ${commonStyles.gradients.discord} group-hover:scale-110 transition-transform duration-200`}> <svg className="w-5 h-5 text-banquise-blue-lightest" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg className="w-5 h-5 text-white" fill="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"/> <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> </svg>
</div> }
<div> title="Discord"
<span className="font-semibold text-lg">Discord</span> description="Rejoindre la communauté"
<p className="text-white/60 text-sm">Rejoindre la communauté</p> href={URLS.social.discord}
</div> isExternal={true}
</div> />
<svg className="w-5 h-5 ml-auto opacity-50 group-hover:opacity-100 group-hover:translate-x-1 transition-all duration-200" 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>
</div> </div>
{/* CTA Button */} {/* CTA Button */}
<div className="pt-6 border-t border-banquise-blue-lightest/20"> <div className="mt-8 pb-6">
<a <Button
href={URLS.services.auth} variant="primary"
target="_blank" size="lg"
rel="noopener noreferrer" leftIcon={
className={`w-full ${commonStyles.buttons.primary} ${commonStyles.gradients.primary} py-4 px-6 text-lg shadow-xl border border-banquise-blue-lightest/20`} <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
onClick={onClose}
>
<svg className="w-6 h-6 mr-3" 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" /> <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> </svg>
}
onClick={() => {
window.open(URLS.services.auth, '_blank');
onClose();
}}
className="w-full shadow-xl"
>
Se connecter Se connecter
</a> </Button>
</div> </div>
</div> </div>
{/* Footer */} {/* Effet de gradient overlay */}
<div className="absolute bottom-6 left-6 right-6"> <div className="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-banquise-blue-dark/10 pointer-events-none" />
<div className="text-center py-4 border-t border-banquise-blue-lightest/20">
<p className="text-white/50 text-sm">
© 2024 {SITE_CONFIG.name}
</p>
</div>
</div>
{/* Effet glassmorphism */}
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-banquise-blue-dark/20 pointer-events-none"></div>
</div> </div>
</div> </div>
); );

View File

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

View File

@ -3,8 +3,14 @@ import { MobileMenu } from './MobileMenu';
import banquiseServer from '/src/assets/banquise_server.svg' import banquiseServer from '/src/assets/banquise_server.svg'
import { URLS, SITE_CONFIG } from '../../config/constants'; import { URLS, SITE_CONFIG } from '../../config/constants';
import { commonStyles } from '../../styles/components'; import { commonStyles } from '../../styles/components';
import type { Translation } from '../../types/i18n';
export const Navigation: React.FC = () => { interface NavigationProps {
translations: Translation['navigation'];
languageSwitcher: React.ReactElement;
}
export const Navigation: React.FC<NavigationProps> = ({ translations, languageSwitcher }) => {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [scrolled, setScrolled] = useState(false); const [scrolled, setScrolled] = useState(false);
@ -13,7 +19,6 @@ export const Navigation: React.FC = () => {
const isScrolled = window.scrollY > 20; const isScrolled = window.scrollY > 20;
setScrolled(isScrolled); setScrolled(isScrolled);
}; };
window.addEventListener('scroll', handleScroll); window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll);
}, []); }, []);
@ -24,7 +29,6 @@ export const Navigation: React.FC = () => {
setMobileMenuOpen(false); setMobileMenuOpen(false);
} }
}; };
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize);
}, []); }, []);
@ -38,7 +42,6 @@ export const Navigation: React.FC = () => {
}`}> }`}>
<div className={commonStyles.layout.container}> <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"> <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 */} {/* Logo section */}
<div className="flex items-center space-x-3 sm:space-x-4 group"> <div className="flex items-center space-x-3 sm:space-x-4 group">
<div className="relative"> <div className="relative">
@ -62,19 +65,29 @@ export const Navigation: React.FC = () => {
{/* Navigation links desktop */} {/* Navigation links desktop */}
<div className="hidden md:flex items-center space-x-1 lg:space-x-2"> <div className="hidden md:flex items-center space-x-1 lg:space-x-2">
<a href="#services" className={commonStyles.nav.link}> <a href="#home" className={commonStyles.nav.link}>
<span className="relative z-10">Services</span> <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> <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>
<a href="#about" className={commonStyles.nav.link}> <a href="#about" className={commonStyles.nav.link}>
<span className="relative z-10">À propos</span> <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> <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>
</div> </div>
{/* Action buttons desktop */} {/* Action buttons desktop */}
<div className="hidden md:flex items-center space-x-3 lg:space-x-4"> <div className="hidden md:flex items-center space-x-3 lg:space-x-4">
{/* Language switcher */}
{languageSwitcher}
<a <a
href={URLS.social.discord} href={URLS.social.discord}
target="_blank" target="_blank"
@ -89,7 +102,6 @@ export const Navigation: React.FC = () => {
<span>Discord</span> <span>Discord</span>
</div> </div>
</a> </a>
<a <a
href={URLS.services.auth} href={URLS.services.auth}
target="_blank" target="_blank"
@ -122,14 +134,13 @@ export const Navigation: React.FC = () => {
aria-expanded={mobileMenuOpen} aria-expanded={mobileMenuOpen}
> >
<div className="w-6 h-6 relative"> <div className="w-6 h-6 relative">
<span className={`absolute block w-6 h-0.5 bg-white transition-all duration-300 ${mobileMenuOpen ? 'rotate-45 top-3' : 'top-1'}`}></span> <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 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> <span className={`absolute block w-6 h-0.5 bg-white transition-all duration-300 ${mobileMenuOpen ? '-rotate-45 top-3' : 'top-5'}`}></span>
</div> </div>
</button> </button>
</div> </div>
</div> </div>
{/* Glassmorphism effect bar */} {/* 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> <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> </nav>
@ -141,6 +152,7 @@ export const Navigation: React.FC = () => {
<MobileMenu <MobileMenu
isOpen={mobileMenuOpen} isOpen={mobileMenuOpen}
onClose={() => setMobileMenuOpen(false)} onClose={() => setMobileMenuOpen(false)}
translations={translations}
/> />
</> </>
); );

View File

@ -0,0 +1,74 @@
import React from 'react';
import { Button } from '../../common/Button';
import { mergeClasses as cn } from '../../../styles/designSystem';
import { URLS } from '../../../config/constants';
interface ActionButtonsProps {
scrolled: boolean;
languageSwitcher: React.ReactElement;
className?: string;
}
export const ActionButtons: React.FC<ActionButtonsProps> = ({
scrolled,
languageSwitcher,
className
}) => {
return (
<div className={cn('hidden md:flex items-center space-x-3', className)}>
{/* Language Switcher */}
<div className="relative">
{languageSwitcher}
</div>
{/* Discord Button */}
<Button
variant="discord"
size="sm"
leftIcon={
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.211.375-.445.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03z"/>
</svg>
}
onClick={() => window.open(URLS.social.discord, '_blank')}
className="hidden lg:flex"
>
Discord
</Button>
{/* Discord Icon Only (tablet) */}
<Button
variant="discord"
size="sm"
onClick={() => window.open(URLS.social.discord, '_blank')}
className="lg:hidden"
aria-label="Rejoindre Discord"
>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.211.375-.445.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03z"/>
</svg>
</Button>
{/* Auth Button */}
<Button
variant="primary"
size="sm"
leftIcon={
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
}
onClick={() => window.open(URLS.services.auth, '_blank')}
className={cn(
'transition-all duration-300',
scrolled
? 'shadow-md hover:shadow-lg'
: 'shadow-lg hover:shadow-xl'
)}
>
<span className="hidden lg:inline">Connexion</span>
<span className="lg:hidden">Se connecter</span>
</Button>
</div>
);
};

View File

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

View File

@ -0,0 +1,49 @@
import React from 'react';
import { mergeClasses as cn } from '../../../styles/designSystem';
interface MobileMenuButtonProps {
isOpen: boolean;
onClick: () => void;
className?: string;
}
export const MobileMenuButton: React.FC<MobileMenuButtonProps> = ({
isOpen,
onClick,
className
}) => {
return (
<button
className={cn(
'md:hidden relative p-3 rounded-xl transition-all duration-300 group',
'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',
className
)}
onClick={onClick}
aria-label={isOpen ? "Fermer le menu" : "Ouvrir le menu"}
aria-expanded={isOpen}
>
{/* Hamburger Icon avec animation moderne */}
<div className="w-6 h-6 relative flex flex-col justify-center items-center">
<span className={cn(
'absolute block h-0.5 w-6 bg-white rounded-full transition-all duration-300 ease-out transform',
isOpen ? 'rotate-45 translate-y-0' : '-translate-y-2'
)} />
<span className={cn(
'absolute block h-0.5 w-6 bg-white rounded-full transition-all duration-300 ease-out',
isOpen ? 'opacity-0 scale-0' : 'opacity-100 scale-100'
)} />
<span className={cn(
'absolute block h-0.5 w-6 bg-white rounded-full transition-all duration-300 ease-out transform',
isOpen ? '-rotate-45 translate-y-0' : 'translate-y-2'
)} />
</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" />
</button>
);
};

View File

@ -0,0 +1,174 @@
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, scrolled, className }) => {
const [activeSection, setActiveSection] = React.useState<string>('home');
// Observer pour détecter la section active
React.useEffect(() => {
const handleScroll = () => {
const scrollPosition = window.scrollY;
const windowHeight = window.innerHeight;
// Si on est en haut de la page (moins de 100px du haut), on active "home"
if (scrollPosition < 100) {
setActiveSection('home');
return;
}
// Sinon, on utilise l'intersection observer logic
const sections = ['home', 'services', 'about'];
let currentSection = 'home';
sections.forEach((sectionId) => {
const element = document.getElementById(sectionId);
if (element) {
const rect = element.getBoundingClientRect();
const sectionTop = rect.top + scrollPosition;
// Si la section est visible dans le viewport
if (scrollPosition >= sectionTop - windowHeight / 3) {
currentSection = sectionId;
}
}
});
setActiveSection(currentSection);
};
// Écouter le scroll
window.addEventListener('scroll', handleScroll);
// Appeler une fois au chargement
handleScroll();
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
// Observer pour détecter la section active avec IntersectionObserver (fallback)
React.useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && window.scrollY > 100) {
setActiveSection(entry.target.id);
}
});
},
{
threshold: 0.3,
rootMargin: '-100px 0px -100px 0px'
}
);
const sections = ['home', 'services', 'about'];
sections.forEach((id) => {
const element = document.getElementById(id);
if (element) observer.observe(element);
});
return () => observer.disconnect();
}, []);
const handleNavClick = (sectionId: string) => {
if (sectionId === 'home') {
// Scroll to top for home section
window.scrollTo({
top: 0,
behavior: 'smooth'
});
} else if (sectionId === 'contact') {
// Open email client for contact
window.location.href = 'mailto:contact@la-banquise.fr';
} else {
// Scroll to specific section
const element = document.getElementById(sectionId);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
}
};
return (
<nav className={cn('hidden md:flex items-center space-x-1', className)}>
<NavLink
href="#home"
isActive={activeSection === 'home'}
onClick={() => handleNavClick('home')}
>
{translations.home}
</NavLink>
<NavLink
href="#services"
isActive={activeSection === 'services'}
onClick={() => handleNavClick('services')}
>
{translations.services}
</NavLink>
<NavLink
href="#about"
isActive={activeSection === 'about'}
onClick={() => handleNavClick('about')}
>
{translations.about}
</NavLink>
<NavLink
href="mailto:contact@la-banquise.fr"
isActive={false}
onClick={() => handleNavClick('contact')}
>
{translations.contact}
</NavLink>
</nav>
);
};

View File

@ -9,11 +9,10 @@ interface AboutSectionProps {
} }
export const AboutSection: React.FC<AboutSectionProps> = ({ openAccordion, toggleAccordion }) => ( export const AboutSection: React.FC<AboutSectionProps> = ({ openAccordion, toggleAccordion }) => (
<section id="about" className="relative bg-gradient-to-b from-banquise-blue-dark/15 to-banquise-blue-dark/20 backdrop-blur-lg py-16 sm:py-20 md:py-24 px-4 sm:px-6 md:px-8 z-2 border-t border-banquise-blue-lightest/20 w-full box-border"> <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"> <div className="max-w-4xl mx-auto">
{/* Header */} {/* Header */}
<div className="text-center mb-12 sm:mb-16 md:mb-20"> <div className="text-center mb-12 sm:mb-16 md:mb-20">
<div className={commonStyles.layout.divider}></div>
<h2 className={`${commonStyles.text.headingXl} mb-6 sm:mb-8 px-2`} style={{ textShadow: '0 2px 4px rgba(0, 0, 0, 0.2)' }}> <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 À Propos de La Banquise
</h2> </h2>
@ -80,6 +79,38 @@ export const AboutSection: React.FC<AboutSectionProps> = ({ openAccordion, toggl
<p className="text-banquise-gray/80 text-sm">Interface de gestion pour serveurs de jeux</p> <p className="text-banquise-gray/80 text-sm">Interface de gestion pour serveurs de jeux</p>
</div> </div>
</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> </div>
<p className={`${commonStyles.text.muted} mt-4`}> <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. Tous nos services sont maintenus avec soin et régulièrement mis à jour pour garantir une expérience optimale.

View File

@ -1,8 +1,13 @@
import React from 'react'; import React from 'react';
import banquiseServer from '/src/assets/banquise_server.svg' import banquiseServer from '/src/assets/banquise_server.svg'
import type { Translation } from '../../types/i18n';
export const HeroSection: React.FC = () => ( interface HeroSectionProps {
<section 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"> 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"> <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 <img
src={banquiseServer} src={banquiseServer}
@ -15,15 +20,15 @@ export const HeroSection: React.FC = () => (
</div> </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)' }}> <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)' }}>
Bienvenue sur La Banquise {translations.title}
</h1> </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)' }}> <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)' }}>
Association d'hébergement et lab réseau pour tous les étudiants et associations de l'EPITA ! {translations.subtitle}
</p> </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"> <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">Découvrir nos services</span> <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> <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> </a>
</section> </section>

View File

@ -1,22 +1,21 @@
import React from 'react'; import React from 'react';
import { ServiceCard } from '../common/ServiceCard';
import { componentStyles } from '../../styles/designSystem';
import type { Service } from '../../types/service';
// Declare the Service interface here
interface Service {
name: string;
url: string;
image: string;
icon: string;
description: string;
features: string[];
}
// Define interface directly in the component file
interface ServicesSectionProps { interface ServicesSectionProps {
services: Service[]; services: Service[];
onServiceClick: (service: Service) => void; onServiceClick: (service: Service) => void;
translations: {
discoverFeatures: string;
};
} }
export const ServicesSection: React.FC<ServicesSectionProps> = ({ services, onServiceClick }) => ( export const ServicesSection: React.FC<ServicesSectionProps> = ({
services,
onServiceClick,
translations
}) => (
<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"> <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> <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)' }}> <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)' }}>
@ -28,35 +27,12 @@ export const ServicesSection: React.FC<ServicesSectionProps> = ({ services, onSe
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 sm:gap-8 w-full"> <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 sm:gap-8 w-full">
{services.map((service) => ( {services.map((service) => (
<div <ServiceCard
key={service.name} key={service.name}
className="group relative bg-gradient-to-br from-banquise-blue-dark/10 to-banquise-blue-dark/5 backdrop-blur-lg rounded-2xl p-6 sm:p-8 border border-banquise-blue-lightest/30 transition-all duration-300 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={service}
onClick={() => onServiceClick(service)} onServiceClick={onServiceClick}
> discoverFeaturesText={translations.discoverFeatures}
{/* 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">
{service.icon}
</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 teaser description */}
<p className="text-banquise-gray/80 leading-relaxed mb-6 sm:mb-8 text-center text-sm sm:text-base">
{service.description.split('.')[0]}.
</p>
{/* CTA */}
<div className="flex items-center justify-center text-banquise-blue-light font-bold group-hover:text-banquise-blue-lightest transition-colors duration-300 text-sm sm:text-base">
<span className="text-center">Découvrir toutes les fonctionnalités</span>
<span className="ml-2 text-lg transition-transform duration-300 group-hover:translate-x-2"></span>
</div>
{/* Subtle 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>
))} ))}
</div> </div>
</section> </section>

View File

@ -1,12 +1,5 @@
import React from 'react'; import React from 'react';
import type { AccordionItemProps } from '../../types';
// Définir l'interface localement :
interface AccordionItemProps {
title: string;
children: React.ReactNode;
isOpen: boolean;
onToggle: () => void;
}
export const AccordionItem: React.FC<AccordionItemProps> = ({ title, children, isOpen, onToggle }) => ( 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={`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`}>

View File

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

View File

@ -0,0 +1,110 @@
import React, { useState } from 'react';
import { mergeClasses as cn } from '../../styles/designSystem';
import type { Language } from '../../types/i18n';
interface ModernLanguageSwitcherProps {
currentLanguage: Language;
onLanguageChange: (language: Language) => void;
availableLanguages: Language[];
}
export const ModernLanguageSwitcher: React.FC<ModernLanguageSwitcherProps> = ({
currentLanguage,
onLanguageChange,
availableLanguages
}) => {
const [isOpen, setIsOpen] = useState(false);
const languageConfig: Record<Language, { name: string; flag: string; nativeName: string }> = {
fr: { name: 'Français', flag: '🇫🇷', nativeName: 'FR' },
en: { name: 'English', flag: '🇬🇧', nativeName: 'EN' },
};
const currentConfig = languageConfig[currentLanguage];
return (
<div className="relative">
{/* Trigger Button */}
<button
onClick={() => setIsOpen(!isOpen)}
className={cn(
'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',
'group'
)}
aria-expanded={isOpen}
aria-haspopup="listbox"
>
<span className="text-lg">{currentConfig.flag}</span>
<span className="hidden sm:inline">{currentConfig.nativeName}</span>
{/* Chevron Icon */}
<svg
className={cn(
'w-4 h-4 transition-transform duration-200',
isOpen ? 'rotate-180' : 'rotate-0'
)}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{/* 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'
)}>
{availableLanguages.map((lang) => {
const config = languageConfig[lang];
const isSelected = lang === currentLanguage;
return (
<button
key={lang}
onClick={() => {
onLanguageChange(lang);
setIsOpen(false);
}}
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',
'focus:outline-none',
isSelected
? 'text-banquise-blue-dark font-semibold bg-banquise-blue/10'
: 'text-gray-700 hover:text-banquise-blue-dark'
)}
role="option"
aria-selected={isSelected}
>
<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">
<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>
)}
</button>
);
})}
</div>
</>
)}
</div>
);
};

View File

@ -1,146 +0,0 @@
import React, { useEffect, useState } from 'react';
export const ParallaxBackground: React.FC = () => {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handleScroll = () => {
setScrollY(window.scrollY);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// Éléments flottants avec différentes vitesses de parallaxe
const floatingElements = [
// Serveurs et équipements
{ icon: '🖥️', x: 10, y: 20, speed: 0.3, size: 'text-2xl', opacity: 0.1 },
{ icon: '🖲️', x: 85, y: 15, speed: 0.2, size: 'text-xl', opacity: 0.08 },
{ icon: '⚙️', x: 75, y: 45, speed: 0.4, size: 'text-3xl', opacity: 0.12 },
{ icon: '🔧', x: 15, y: 60, speed: 0.25, size: 'text-lg', opacity: 0.06 },
{ icon: '💾', x: 90, y: 70, speed: 0.35, size: 'text-2xl', opacity: 0.1 },
// Code et développement
{ icon: '<>', x: 30, y: 35, speed: 0.15, size: 'text-xl', opacity: 0.08, isText: true },
{ icon: '{ }', x: 60, y: 25, speed: 0.28, size: 'text-2xl', opacity: 0.1, isText: true },
{ icon: '#!/bin', x: 5, y: 80, speed: 0.2, size: 'text-sm', opacity: 0.06, isText: true },
{ icon: 'git', x: 80, y: 85, speed: 0.32, size: 'text-lg', opacity: 0.08, isText: true },
// Réseau et connectivité
{ icon: '🌐', x: 45, y: 10, speed: 0.22, size: 'text-2xl', opacity: 0.09 },
{ icon: '🔗', x: 25, y: 75, speed: 0.18, size: 'text-xl', opacity: 0.07 },
{ icon: '📡', x: 70, y: 55, speed: 0.26, size: 'text-lg', opacity: 0.08 },
// Sécurité
{ icon: '🔒', x: 55, y: 40, speed: 0.3, size: 'text-xl', opacity: 0.09 },
{ icon: '🛡️', x: 35, y: 65, speed: 0.24, size: 'text-2xl', opacity: 0.1 },
{ icon: '🔑', x: 85, y: 30, speed: 0.16, size: 'text-lg', opacity: 0.07 },
// Données et stockage
{ icon: '💿', x: 20, y: 45, speed: 0.28, size: 'text-xl', opacity: 0.08 },
{ icon: '📊', x: 65, y: 75, speed: 0.22, size: 'text-2xl', opacity: 0.09 },
{ icon: '📈', x: 40, y: 20, speed: 0.34, size: 'text-lg', opacity: 0.07 },
// Éléments techniques supplémentaires
{ icon: 'sudo', x: 12, y: 90, speed: 0.19, size: 'text-sm', opacity: 0.06, isText: true },
{ icon: 'SSH', x: 78, y: 12, speed: 0.31, size: 'text-base', opacity: 0.08, isText: true },
{ icon: 'API', x: 92, y: 50, speed: 0.27, size: 'text-lg', opacity: 0.09, isText: true },
{ icon: 'TCP', x: 8, y: 30, speed: 0.23, size: 'text-base', opacity: 0.07, isText: true },
{ icon: 'HTTP', x: 50, y: 80, speed: 0.29, size: 'text-sm', opacity: 0.06, isText: true },
];
return (
<div className="fixed inset-0 pointer-events-none overflow-hidden z-0">
{/* Grille de fond subtile */}
<div className="absolute inset-0 opacity-[0.02]">
<div
className="absolute inset-0 bg-gradient-to-br from-banquise-blue-dark/20 to-transparent"
style={{
backgroundImage: `
linear-gradient(rgba(31, 93, 137, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(31, 93, 137, 0.03) 1px, transparent 1px)
`,
backgroundSize: '50px 50px',
transform: `translateY(${scrollY * 0.1}px)`
}}
/>
</div>
{/* Particules de code flottantes */}
<div className="absolute inset-0">
{floatingElements.map((element, index) => (
<div
key={index}
className={`absolute ${element.size} font-mono select-none transition-all duration-1000 ease-out`}
style={{
left: `${element.x}%`,
top: `${element.y}%`,
transform: `translateY(${scrollY * element.speed}px) rotate(${scrollY * 0.01}deg)`,
opacity: element.opacity,
color: element.isText ? '#a8daff' : 'inherit',
textShadow: element.isText ? '0 0 10px rgba(168, 218, 255, 0.3)' : 'none',
filter: 'blur(0.5px)',
animation: `float-${index % 3} ${6 + (index % 4)}s ease-in-out infinite`
}}
>
{element.icon}
</div>
))}
</div>
{/* Lignes de connexion animées */}
<svg
className="absolute inset-0 w-full h-full opacity-[0.03]"
style={{
transform: `translateY(${scrollY * 0.2}px)`
}}
>
<defs>
<linearGradient id="lineGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#a8daff" stopOpacity="0" />
<stop offset="50%" stopColor="#a8daff" stopOpacity="0.5" />
<stop offset="100%" stopColor="#a8daff" stopOpacity="0" />
</linearGradient>
</defs>
{/* Lignes de connexion entre les éléments */}
{[...Array(8)].map((_, i) => (
<line
key={i}
x1={`${10 + i * 12}%`}
y1={`${20 + i * 8}%`}
x2={`${30 + i * 15}%`}
y2={`${40 + i * 12}%`}
stroke="url(#lineGradient)"
strokeWidth="1"
className="animate-pulse"
style={{
animationDelay: `${i * 0.5}s`,
animationDuration: `${3 + i}s`
}}
/>
))}
</svg>
{/* Cercles de données en mouvement */}
<div className="absolute inset-0">
{[...Array(6)].map((_, i) => (
<div
key={i}
className="absolute rounded-full border border-banquise-blue-lightest/5 animate-ping"
style={{
left: `${15 + i * 15}%`,
top: `${25 + i * 12}%`,
width: `${40 + i * 20}px`,
height: `${40 + i * 20}px`,
transform: `translateY(${scrollY * (0.1 + i * 0.05)}px)`,
animationDelay: `${i * 1.2}s`,
animationDuration: `${4 + i}s`
}}
/>
))}
</div>
</div>
);
};

View File

@ -1,21 +1,15 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { URLS } from '../../config/constants'; import { URLS } from '../../config/constants';
import type { Service } from '../../types/service';
interface Service { import type { Translation } from '../../types/i18n';
name: string;
url: string;
image: string;
icon: string;
description: string;
features: string[];
}
interface PopupProps { interface PopupProps {
service: Service; service: Service;
onClose: () => void; onClose: () => void;
translations: Translation['common'];
} }
export const Popup: React.FC<PopupProps> = ({ service, onClose }) => { export const Popup: React.FC<PopupProps> = ({ service, onClose, translations }) => {
// Empêcher le scroll du body quand la popup est ouverte // Empêcher le scroll du body quand la popup est ouverte
useEffect(() => { useEffect(() => {
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
@ -34,7 +28,7 @@ export const Popup: React.FC<PopupProps> = ({ service, onClose }) => {
<button <button
onClick={onClose} 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" 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="Fermer la popup" aria-label={translations.close}
> >
× ×
</button> </button>
@ -100,7 +94,7 @@ export const Popup: React.FC<PopupProps> = ({ service, onClose }) => {
{/* Fonctionnalités */} {/* 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"> <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> <span className="text-xl sm:text-2xl lg:text-3xl mr-3"></span>
Fonctionnalités principales {translations.discoverFeatures}
</h3> </h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
{service.features.map((feature, index) => ( {service.features.map((feature, index) => (

View File

@ -1,24 +1,8 @@
import React, { useState, useEffect } from 'react'; import React from 'react';
import { useScrollEffects } from '../../hooks/useScrollEffects';
export const ScrollToTopButton: React.FC = () => { export const ScrollToTopButton: React.FC = () => {
const [isVisible, setIsVisible] = useState(false); const { isVisible, scrollToTop } = useScrollEffects();
useEffect(() => {
const toggleVisibility = () => {
// Afficher le bouton après avoir scrollé 300px
setIsVisible(window.scrollY > 300);
};
window.addEventListener('scroll', toggleVisibility);
return () => window.removeEventListener('scroll', toggleVisibility);
}, []);
const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
};
return ( return (
<button <button

View File

@ -3,10 +3,15 @@ export const URLS = {
wiki: "https://wiki.la-banquise.fr", wiki: "https://wiki.la-banquise.fr",
gitea: "https://git.la-banquise.fr", gitea: "https://git.la-banquise.fr",
panel: "https://panel.la-banquise.fr", panel: "https://panel.la-banquise.fr",
auth: "https://auth.la-banquise.fr" auth: "https://auth.la-banquise.fr",
pelican: "https://pelican.la-banquise.fr",
intra: "https://intra.la-banquise.fr",
mails: "https://mails.la-banquise.fr",
opencloud: "https://opencloud.la-banquise.fr",
ssp: "https://ssp.la-banquise.fr"
}, },
social: { social: {
discord: "https://discord.gg/labanquise" discord: "https://discord.gg/bJhM97wans"
}, },
contact: { contact: {
email: "mailto:contact@la-banquise.fr" email: "mailto:contact@la-banquise.fr"

View File

@ -0,0 +1,153 @@
import type { Translation } from '../../types/i18n';
import { URLS } from '../../config/constants';
export const en: Translation = {
services: [
{
name: "Wiki",
url: URLS.services.wiki,
image: "/path/to/wiki-image.jpg",
icon: "📚",
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",
"Versioning system to track changes",
"Real-time collaboration with multiple contributors",
"Smart search across all documents",
"Predefined templates for different documentation types",
"Comment and revision system",
"PDF and HTML export for external sharing",
"Git integration for backup"
]
},
{
name: "Gitea",
url: URLS.services.gitea,
image: "/path/to/gitea-image.jpg",
icon: "🔧",
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",
"Intuitive web interface for project management",
"Issues and pull requests with review system",
"Integrated wiki for each project",
"CI/CD actions for automation",
"Fine-grained permissions and team management",
"Complete REST API for integration",
"Webhooks for external notifications"
]
},
{
name: "Gaming Panel",
url: URLS.services.panel,
image: "/path/to/panel-image.jpg",
icon: "🎮",
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...)",
"One-click deployment with pre-configured templates",
"Real-time administration console",
"File management with integrated editor",
"Performance and resource monitoring",
"Automatic backup system",
"Automated task scheduler"
]
},
{
name: "Pelican",
url: URLS.services.pelican,
image: "/path/to/pelican-image.jpg",
icon: "🐧",
description: "Ultra-fast and flexible Python static site generator. Create blogs, portfolios or documentation sites with optimal performance and simplified deployment.",
features: [
"Game server management with dedicated servers (Minecraft, CS2, Palworld...)",
"One-click deployment with pre-configured templates",
"Real-time administration console",
"File management with integrated editor",
"Performance and resource monitoring",
"Automatic backup system",
"Automated task scheduler"
]
},
{
name: "Intranet",
url: URLS.services.intra,
image: "/path/to/intra-image.jpg",
icon: "🏢",
description: "Secure private space for the association to centralize internal resources, communications and collaboration tools between members.",
features: [
"Personalized dashboard for each member",
"Event and meeting calendar",
"Secure file sharing",
"Private discussion forums",
"Member directory with profiles",
"Internal notification system",
"Project and task management",
"Archive of decisions and minutes"
]
},
{
name: "Mails",
url: URLS.services.mails,
image: "/path/to/mails-image.jpg",
icon: "📧",
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",
"Modern and responsive webmail interface",
"Integrated anti-spam and antivirus filters",
"Synchronized contacts and calendar",
"IMAP/SMTP support for external clients",
"Generous storage with archiving",
"Communication encryption",
"Automatic data backup"
]
},
{
name: "Password Change",
url: URLS.services.ssp,
image: "/path/to/ssp-image.jpg",
icon: "🔐",
description: "Secure interface for autonomous password management. Easily change your credentials safely.",
features: [
"Secure interface to change your password",
"Password complexity validation",
"Email notifications of changes",
"Modification history"
]
},
{
name: "OpenCloud",
url: URLS.services.opencloud,
image: "/path/to/opencloud-image.jpg",
icon: "☁️",
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",
"Multi-device synchronization",
"File sharing with secure links",
"Collaborative document editing",
"Automatic file versioning",
"Native mobile applications",
"Integration with external tools",
"Geo-redundant data backup"
]
}
],
hero: {
title: "Welcome to La Banquise",
subtitle: "Your trusted technology partner",
cta: "Discover our services"
},
navigation: {
home: "Home",
services: "Services",
about: "About",
contact: "Contact"
},
common: {
discoverFeatures: "Discover all features",
close: "Close",
loading: "Loading..."
}
};

View File

@ -0,0 +1,153 @@
import type { Translation } from '../../types/i18n';
import { URLS } from '../../config/constants';
export const fr: Translation = {
services: [
{
name: "Wiki",
url: URLS.services.wiki,
image: "/path/to/wiki-image.jpg",
icon: "📚",
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",
"Système de versioning pour suivre les modifications",
"Collaboration en temps réel avec plusieurs contributeurs",
"Recherche intelligente dans tous les documents",
"Templates prédéfinis pour différents types de documentation",
"Système de commentaires et de révisions",
"Export PDF et HTML pour partage externe",
"Intégration avec Git pour la sauvegarde"
]
},
{
name: "Gitea",
url: URLS.services.gitea,
image: "/path/to/gitea-image.jpg",
icon: "🔧",
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",
"Interface web intuitive pour la gestion des projets",
"Issues et pull requests avec système de review",
"Wiki intégré pour chaque projet",
"Actions CI/CD pour l'automatisation",
"Gestion fine des permissions et des équipes",
"API REST complète pour l'intégration",
"Webhooks pour les notifications externes"
]
},
{
name: "Panel Gaming",
url: URLS.services.panel,
image: "/path/to/panel-image.jpg",
icon: "🎮",
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...)",
"Déploiement en un clic avec templates préconfigurés",
"Console d'administration en temps réel",
"Gestion des fichiers avec éditeur intégré",
"Monitoring des performances et ressources",
"Système de sauvegarde automatique",
"Planificateur de tâches automatisées"
]
},
{
name: "Pelican",
url: URLS.services.pelican,
image: "/path/to/pelican-image.jpg",
icon: "🐧",
description: "Générateur de sites statiques Python ultra-rapide et flexible. Créez des blogs, portfolios ou sites de documentation avec une performance optimale et un déploiement simplifié.",
features: [
"Gestion de serveurs de jeux avec serveurs dédiés (Minecraft, CS2, Palworld...)",
"Déploiement en un clic avec templates préconfigurés",
"Console d'administration en temps réel",
"Gestion des fichiers avec éditeur intégré",
"Monitoring des performances et ressources",
"Système de sauvegarde automatique",
"Planificateur de tâches automatisées"
]
},
{
name: "Intranet",
url: URLS.services.intra,
image: "/path/to/intra-image.jpg",
icon: "🏢",
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",
"Calendrier des événements et réunions",
"Partage de fichiers sécurisé",
"Forums de discussion privés",
"Annuaire des membres avec profils",
"Système de notifications internes",
"Gestion des projets et tâches",
"Archive des décisions et procès-verbaux"
]
},
{
name: "Mails",
url: URLS.services.mails,
image: "/path/to/mails-image.jpg",
icon: "📧",
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",
"Interface webmail moderne et responsive",
"Filtres anti-spam et antivirus intégrés",
"Contacts et calendrier synchronisés",
"Support IMAP/SMTP pour clients externes",
"Stockage généreux avec archivage",
"Chiffrement des communications",
"Sauvegarde automatique des données"
]
},
{
name: "Changement de mot de passe",
url: URLS.services.ssp,
image: "/path/to/ssp-image.jpg",
icon: "🔐",
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",
"Validation de la complexité des mots de passe",
"Notifications par email des changements",
"Historique des modifications"
]
},
{
name: "OpenCloud",
url: URLS.services.opencloud,
image: "/path/to/opencloud-image.jpg",
icon: "☁️",
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é",
"Synchronisation multi-appareils",
"Partage de fichiers avec liens sécurisés",
"Édition collaborative de documents",
"Versioning automatique des fichiers",
"Applications mobiles natives",
"Intégration avec outils externes",
"Sauvegarde géoredondante des données"
]
}
],
hero: {
title: "Bienvenue chez La Banquise",
subtitle: "Votre partenaire technologique de confiance",
cta: "Découvrir nos services"
},
navigation: {
home: "Accueil",
services: "Services",
about: "À propos",
contact: "Contact"
},
common: {
discoverFeatures: "Découvrir toutes les fonctionnalités",
close: "Fermer",
loading: "Chargement..."
}
};

View File

@ -0,0 +1,14 @@
import { fr } from './fr';
import { en } from './en';
import type { Language, Translation } from '../../types/i18n';
export const translations: Record<Language, Translation> = {
fr,
en,
// Ajoutez d'autres langues ici :
// es,
// de,
};
export const defaultLanguage: Language = 'fr';
export const availableLanguages: Language[] = ['fr', 'en'];

View File

@ -0,0 +1,33 @@
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
*/
export const useAccordion = (initialState: string | null = null) => {
const [openAccordion, setOpenAccordion] = useState<string | null>(initialState);
const toggleAccordion = useCallback((title: string) => {
setOpenAccordion(prev => prev === title ? null : title);
}, []);
const openSpecificAccordion = useCallback((title: string) => {
setOpenAccordion(title);
}, []);
const closeAccordion = useCallback(() => {
setOpenAccordion(null);
}, []);
const isOpen = useCallback((title: string) => {
return openAccordion === title;
}, [openAccordion]);
return {
openAccordion,
toggleAccordion,
openSpecificAccordion,
closeAccordion,
isOpen,
};
};

View File

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

View File

@ -0,0 +1,49 @@
import { useState, useEffect, useCallback } from 'react';
/**
* Hook personnalisé pour gérer les effets de scroll
* Remplace la logique répétée dans Navigation.tsx et ScrollToTopButton.tsx
*/
export const useScrollEffects = () => {
const [scrolled, setScrolled] = useState(false);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const handleScroll = () => {
const scrollY = window.scrollY;
setScrolled(scrollY > 20);
setIsVisible(scrollY > 300);
};
window.addEventListener('scroll', handleScroll, { passive: true });
// Call once to set initial state
handleScroll();
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const scrollToTop = useCallback(() => {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
}, []);
const scrollToElement = useCallback((elementId: string) => {
const element = document.getElementById(elementId);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
}, []);
return {
scrolled,
isVisible,
scrollToTop,
scrollToElement,
};
};

View File

@ -0,0 +1,27 @@
import { useState, useCallback } from 'react';
import type { Service } from '../types/service';
/**
* Hook personnalisé pour gérer l'état des modales de services
* Remplace la logique dans App.tsx et simplifie la gestion d'état
*/
export const useServiceModal = () => {
const [selectedService, setSelectedService] = useState<Service | null>(null);
const openServiceModal = useCallback((service: Service) => {
setSelectedService(service);
}, []);
const closeServiceModal = useCallback(() => {
setSelectedService(null);
}, []);
const isModalOpen = selectedService !== null;
return {
selectedService,
openServiceModal,
closeServiceModal,
isModalOpen,
};
};

View File

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

View File

@ -1,5 +1,10 @@
// 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 = { export const commonStyles = {
// Gradients // Gradients - Keep existing structure
gradients: { gradients: {
primary: "bg-gradient-to-r from-banquise-blue to-banquise-blue-light", primary: "bg-gradient-to-r from-banquise-blue to-banquise-blue-light",
primaryBr: "bg-gradient-to-br from-banquise-blue to-banquise-blue-light", primaryBr: "bg-gradient-to-br from-banquise-blue to-banquise-blue-light",
@ -9,53 +14,49 @@ export const commonStyles = {
discordHover: "hover:from-indigo-500 hover:to-purple-500" discordHover: "hover:from-indigo-500 hover:to-purple-500"
}, },
// Buttons // Buttons - Keep existing structure
buttons: { 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", 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", 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" 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 // Cards - Keep existing structure
cards: { cards: {
base: "backdrop-blur-lg rounded-2xl border border-banquise-blue-lightest/30 transition-all duration-300", 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", hover: "hover:shadow-xl hover:border-banquise-blue-lightest/50",
interactive: "cursor-pointer hover:-translate-y-4 hover:shadow-2xl active:scale-95" interactive: "cursor-pointer hover:-translate-y-4 hover:shadow-2xl active:scale-95"
}, },
// Text - Hiérarchie améliorée // Text - Keep existing structure
text: { text: {
heading: "font-heading font-bold tracking-tight", heading: "font-heading font-bold tracking-tight",
// Titres principaux de section
headingXl: "text-3xl sm:text-4xl md:text-5xl text-banquise-gray 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", 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", 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", headingSm: "text-lg sm:text-xl md:text-2xl text-banquise-blue-dark font-heading font-semibold tracking-tight",
// Sous-titres
subheading: "text-base sm:text-lg md:text-xl text-banquise-gray/90 font-medium leading-relaxed", subheading: "text-base sm:text-lg md:text-xl text-banquise-gray/90 font-medium leading-relaxed",
// Corps de texte
body: "text-sm sm:text-base md:text-lg text-banquise-blue-dark/90 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", description: "text-banquise-gray/80 leading-relaxed",
muted: "text-banquise-gray/90 leading-relaxed", muted: "text-banquise-gray/90 leading-relaxed",
// Texte sur fond sombre
lightHeading: "text-banquise-blue-lightest font-heading font-bold tracking-tight", lightHeading: "text-banquise-blue-lightest font-heading font-bold tracking-tight",
lightBody: "text-white/90 leading-relaxed" lightBody: "text-white/90 leading-relaxed"
}, },
// Layout // Layout - Keep existing structure
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", 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", 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" 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 and decorative elements // Icons - Keep existing structure
icons: { 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", 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" small: "w-10 h-10 rounded-lg flex items-center justify-center text-white"
}, },
// Navigation // Navigation - Keep existing structure
nav: { 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", 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" 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"

View File

@ -0,0 +1,176 @@
// Design System - Centralized design tokens and reusable styles
export const designTokens = {
// Colors
colors: {
banquise: {
blue: '#40B4FF',
blueDark: '#1F5D89',
blueLight: '#69B7E2',
blueLightest: '#A5F0FF',
gray: '#F6F6F6',
}
},
// Spacing
spacing: {
xs: '0.25rem',
sm: '0.5rem',
md: '1rem',
lg: '1.5rem',
xl: '2rem',
xxl: '3rem',
},
// Border radius
borderRadius: {
sm: '0.5rem',
md: '0.75rem',
lg: '1rem',
xl: '1.5rem',
xxl: '2rem',
},
// Shadows
shadows: {
sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
md: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1)',
xxl: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
},
// Typography
typography: {
fontFamily: {
heading: ['Dela Gothic One', 'sans-serif'],
body: ['Roboto', 'sans-serif'],
},
fontSize: {
xs: '0.75rem',
sm: '0.875rem',
base: '1rem',
lg: '1.125rem',
xl: '1.25rem',
'2xl': '1.5rem',
'3xl': '1.875rem',
'4xl': '2.25rem',
'5xl': '3rem',
'6xl': '3.75rem',
},
fontWeight: {
normal: '400',
medium: '500',
semibold: '600',
bold: '700',
extrabold: '800',
},
},
// Transitions
transitions: {
fast: 'all 0.15s ease-in-out',
normal: 'all 0.3s ease-in-out',
slow: 'all 0.5s ease-in-out',
},
// Z-index
zIndex: {
dropdown: 1000,
sticky: 1020,
fixed: 1030,
modalBackdrop: 1040,
modal: 1050,
popover: 1060,
tooltip: 1070,
},
} as const;
// Reusable gradient combinations
export const gradients = {
primary: 'bg-gradient-to-r from-banquise-blue to-banquise-blue-light',
primaryBr: 'bg-gradient-to-br from-banquise-blue to-banquise-blue-light',
card: 'bg-gradient-to-br from-banquise-blue-dark/10 to-banquise-blue-dark/5',
cardHover: 'hover:from-banquise-blue-dark/15 hover:to-banquise-blue-dark/8',
discord: 'bg-gradient-to-r from-indigo-600 to-purple-600',
discordHover: 'hover:from-indigo-500 hover:to-purple-500',
background: 'bg-gradient-to-b from-banquise-blue-dark via-banquise-blue-dark/95 to-banquise-blue-dark',
} as const;
// Reusable component styles
export const componentStyles = {
// Buttons
button: {
base: 'inline-flex items-center justify-center font-bold text-white border-0 rounded-2xl transition-all duration-300 hover:shadow-xl hover:-translate-y-1 hover:scale-105 active:scale-95',
primary: 'bg-gradient-to-r from-banquise-blue to-banquise-blue-light',
discord: 'group relative overflow-hidden px-4 lg:px-6 py-2.5 lg:py-3 text-white font-semibold text-sm lg:text-base rounded-xl bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500',
auth: 'group relative overflow-hidden px-4 lg:px-6 py-2.5 lg:py-3 text-white font-semibold text-sm lg:text-base rounded-xl',
},
// Cards
card: {
base: 'backdrop-blur-lg rounded-2xl border border-banquise-blue-lightest/30 transition-all duration-300',
hover: 'hover:shadow-xl hover:border-banquise-blue-lightest/50',
interactive: 'cursor-pointer hover:-translate-y-4 hover:shadow-2xl active:scale-95',
gradient: 'bg-gradient-to-br from-banquise-blue-dark/10 to-banquise-blue-dark/5',
},
// Navigation
nav: {
link: 'px-4 lg:px-6 py-2.5 lg:py-3 text-white/90 hover:text-white font-medium text-sm lg:text-base rounded-xl transition-all duration-300 hover:bg-white/10 hover:backdrop-blur-sm relative group',
mobileItem: 'group flex items-center p-4 text-white/90 hover:text-white no-underline rounded-2xl hover:bg-gradient-to-r hover:from-banquise-blue/20 hover:to-banquise-blue-light/20 transition-all duration-300 border border-transparent hover:border-banquise-blue-lightest/20',
},
// Text styles
text: {
heading: 'font-heading font-bold tracking-tight',
headingXl: 'text-3xl sm:text-4xl md:text-5xl text-banquise-gray font-heading font-bold tracking-tight',
headingLg: 'text-2xl sm:text-3xl md:text-4xl text-banquise-gray font-heading font-bold tracking-tight',
headingMd: 'text-xl sm:text-2xl md:text-3xl text-banquise-blue-dark font-heading font-bold tracking-tight',
headingSm: 'text-lg sm:text-xl md:text-2xl text-banquise-blue-dark font-heading font-semibold tracking-tight',
subheading: 'text-base sm:text-lg md:text-xl text-banquise-gray/90 font-medium leading-relaxed',
body: 'text-sm sm:text-base md:text-lg text-banquise-blue-dark/90 leading-relaxed',
description: 'text-banquise-gray/80 leading-relaxed',
muted: 'text-banquise-gray/90 leading-relaxed',
lightHeading: 'text-banquise-blue-lightest font-heading font-bold tracking-tight',
lightBody: 'text-white/90 leading-relaxed',
},
// Icons
icon: {
base: 'w-16 h-16 sm:w-20 sm:h-20 lg:w-24 lg:h-24 rounded-2xl flex items-center justify-center text-3xl sm:text-4xl lg:text-5xl shadow-lg',
small: 'w-10 h-10 rounded-lg flex items-center justify-center text-white',
},
// Layout
layout: {
section: 'py-12 sm:py-16 md:py-20 w-full max-w-6xl mx-auto px-4 sm:px-6 md:px-8',
container: 'max-w-6xl mx-auto',
divider: 'w-20 h-1 bg-gradient-to-r from-banquise-blue-lightest to-banquise-blue mx-auto mb-6 sm:mb-8 rounded-full',
},
// Animations
animation: {
hover: 'hover:-translate-y-4 hover:shadow-2xl transition-all duration-300',
scale: 'hover:scale-105 active:scale-95 transition-transform duration-300',
fadeIn: 'animate-fadeIn',
slideUp: 'animate-slideUp',
},
} as const;
// Utility function to merge classes
export const mergeClasses = (...classes: (string | undefined | null | false)[]): string => {
return classes.filter(Boolean).join(' ');
};
// Responsive breakpoints
export const breakpoints = {
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
'2xl': '1536px',
} as const;
export type DesignTokens = typeof designTokens;
export type Gradients = typeof gradients;
export type ComponentStyles = typeof componentStyles;

View File

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

View File

@ -1,11 +1,5 @@
export interface Service { // Re-export types from their specific modules
name: string; export type { Service } from './service';
url: string;
image: string;
description: string;
features: string[];
icon: string;
}
export interface AccordionItemProps { export interface AccordionItemProps {
title: string; title: string;

View File

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

View File

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

View File

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

View File

@ -34,6 +34,10 @@ export default {
'gentle-float': 'gentle-float 6s ease-in-out infinite', 'gentle-float': 'gentle-float 6s ease-in-out infinite',
'fadeIn': 'fadeIn 0.2s ease-out', 'fadeIn': 'fadeIn 0.2s ease-out',
'slideUp': 'slideUp 0.3s 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: { keyframes: {
float: { float: {
@ -92,6 +96,35 @@ export default {
from: { transform: 'translateY(30px)', opacity: '0' }, from: { transform: 'translateY(30px)', opacity: '0' },
to: { transform: 'translateY(0)', opacity: '1' }, 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: { backdropBlur: {
'xs': '2px', 'xs': '2px',

6
package-lock.json generated Normal file
View File

@ -0,0 +1,6 @@
{
"name": "website-front",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}