refactoring & UI improvment

This commit is contained in:
sahamone 2025-07-10 18:35:45 +02:00 committed by Arthur Wambst
parent 312ce16381
commit f286446a2e
No known key found for this signature in database
31 changed files with 1967 additions and 845 deletions

File diff suppressed because it is too large Load Diff

View File

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

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,85 +2,78 @@ import React from 'react';
import { URLS, SITE_CONFIG } from '../../config/constants';
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">
<div className="flex flex-col md:flex-row justify-between max-w-6xl mx-auto gap-6 sm:gap-8">
<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">
Services
</h4>
<ul className="list-none p-0 m-0 space-y-2 sm:space-y-3">
<li>
<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">
Wiki
</a>
</li>
<li>
<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">
Gitea
</a>
</li>
<li>
<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">
Panel
</a>
</li>
<li>
<a href={URLS.services.pelican} 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">
Pelican
</a>
</li>
<li>
<a href={URLS.services.intra} 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">
Intranet
</a>
</li>
<li>
<a href={URLS.services.mails} 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">
Webmail
</a>
</li>
<li>
<a href={URLS.services.opencloud} 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">
OpenCloud
</a>
</li>
</ul>
<footer className="bg-banquise-blue-dark/95 backdrop-blur-sm text-white py-8 px-4 sm:px-6 md:px-8 relative z-10 border-t border-banquise-blue-lightest/10 w-full box-border">
<div className="max-w-6xl mx-auto">
{/* Main Footer Content */}
<div className="flex flex-col md:flex-row justify-between items-center gap-6 mb-6">
{/* Logo/Brand */}
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-gradient-to-br from-banquise-blue-light to-banquise-blue rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-sm">B</span>
</div>
<span className="text-banquise-blue-lightest font-semibold text-lg">
{SITE_CONFIG.name}
</span>
</div>
{/* Quick Links */}
<div className="flex flex-wrap items-center gap-6 text-sm">
<a
href={URLS.services.wiki}
className="text-banquise-gray/80 hover:text-banquise-blue-lightest transition-colors duration-200"
>
Wiki
</a>
<a
href={URLS.services.gitea}
className="text-banquise-gray/80 hover:text-banquise-blue-lightest transition-colors duration-200"
>
Gitea
</a>
<a
href={URLS.services.panel}
className="text-banquise-gray/80 hover:text-banquise-blue-lightest transition-colors duration-200"
>
Panel
</a>
<a
href={URLS.services.opencloud}
className="text-banquise-gray/80 hover:text-banquise-blue-lightest transition-colors duration-200"
>
OpenCloud
</a>
</div>
{/* Social Links */}
<div className="flex items-center gap-4">
<a
href={URLS.social.discord}
className="w-10 h-10 bg-banquise-blue/20 hover:bg-banquise-blue/30 rounded-full flex items-center justify-center transition-all duration-200 hover:scale-110"
aria-label="Discord"
>
<span className="text-banquise-blue-lightest text-sm">💬</span>
</a>
<a
href={URLS.contact.email}
className="w-10 h-10 bg-banquise-blue/20 hover:bg-banquise-blue/30 rounded-full flex items-center justify-center transition-all duration-200 hover:scale-110"
aria-label="Email"
>
<span className="text-banquise-blue-lightest text-sm">📧</span>
</a>
</div>
</div>
<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">
Communauté
</h4>
<ul className="list-none p-0 m-0 space-y-2 sm:space-y-3">
<li>
<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
</a>
</li>
</ul>
{/* Bottom Bar */}
<div className="flex flex-col sm:flex-row justify-between items-center gap-4 pt-6 border-t border-banquise-blue-lightest/5">
<p className="text-banquise-gray/60 text-xs text-center sm:text-left">
© 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 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">
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>
</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 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">
© 2024 {SITE_CONFIG.name}. Tous droits réservés.
</div>
</footer>
);

View File

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

View File

@ -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

@ -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 }) => (
<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">
{/* Header */}
<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)' }}>
À Propos de La Banquise
</h2>

View File

@ -7,7 +7,7 @@ interface HeroSectionProps {
}
export const HeroSection: React.FC<HeroSectionProps> = ({ translations }) => (
<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">
<section id="home" className="min-h-[calc(80vh-72px)] flex flex-col justify-center items-center text-center py-12 sm:py-16 md:py-20 w-full max-w-6xl mx-auto px-4 sm:px-6 md:px-8 relative z-3">
<div className="mb-8 sm:mb-10 md:mb-12 w-32 h-32 sm:w-40 sm:h-40 md:w-48 md:h-48 rounded-full bg-gradient-to-br from-banquise-blue-dark/20 to-banquise-blue/10 p-4 sm:p-5 md:p-6 shadow-2xl backdrop-blur-sm border border-banquise-blue-lightest/30 relative group">
<img
src={banquiseServer}

View File

@ -1,4 +1,6 @@
import React from 'react';
import { ServiceCard } from '../common/ServiceCard';
import { componentStyles } from '../../styles/designSystem';
import type { Service } from '../../types/service';
interface ServicesSectionProps {
@ -25,35 +27,12 @@ export const ServicesSection: React.FC<ServicesSectionProps> = ({
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 sm:gap-8 w-full">
{services.map((service) => (
<div
<ServiceCard
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"
onClick={() => onServiceClick(service)}
>
{/* 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">{translations.discoverFeatures}</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>
service={service}
onServiceClick={onServiceClick}
discoverFeaturesText={translations.discoverFeatures}
/>
))}
</div>
</section>

View File

@ -1,12 +1,5 @@
import React from 'react';
// Définir l'interface localement :
interface AccordionItemProps {
title: string;
children: React.ReactNode;
isOpen: boolean;
onToggle: () => void;
}
import type { AccordionItemProps } from '../../types';
export const AccordionItem: React.FC<AccordionItemProps> = ({ title, children, isOpen, onToggle }) => (
<div className={`bg-gradient-to-br from-banquise-blue-dark/15 to-banquise-blue-dark/5 backdrop-blur-lg rounded-2xl overflow-hidden border border-banquise-blue-lightest/30 transition-all duration-300 shadow-sm ${isOpen ? 'shadow-xl border-banquise-blue-lightest/50 scale-[1.01]' : ''} hover:shadow-lg hover:border-banquise-blue-lightest/40`}>

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,24 +1,8 @@
import React, { useState, useEffect } from 'react';
import React from 'react';
import { useScrollEffects } from '../../hooks/useScrollEffects';
export const ScrollToTopButton: React.FC = () => {
const [isVisible, setIsVisible] = useState(false);
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'
});
};
const { isVisible, scrollToTop } = useScrollEffects();
return (
<button

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

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useMemo } from 'react';
import type { Language, Translation } from '../types/i18n';
import { translations, defaultLanguage } from '../data/translations';
@ -9,10 +9,13 @@ export const useTranslation = () => {
return saved && translations[saved] ? saved : defaultLanguage;
});
const [t, setT] = useState<Translation>(translations[currentLanguage]);
// Memoize the translation object to prevent unnecessary re-renders
const t = useMemo<Translation>(() => translations[currentLanguage], [currentLanguage]);
// Memoize available languages array
const availableLanguages = useMemo(() => Object.keys(translations) as Language[], []);
useEffect(() => {
setT(translations[currentLanguage]);
localStorage.setItem('language', currentLanguage);
}, [currentLanguage]);
@ -26,6 +29,6 @@ export const useTranslation = () => {
t,
currentLanguage,
changeLanguage,
availableLanguages: Object.keys(translations) as Language[]
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 = {
// Gradients
// Gradients - Keep existing structure
gradients: {
primary: "bg-gradient-to-r from-banquise-blue to-banquise-blue-light",
primaryBr: "bg-gradient-to-br from-banquise-blue to-banquise-blue-light",
@ -9,53 +14,49 @@ export const commonStyles = {
discordHover: "hover:from-indigo-500 hover:to-purple-500"
},
// Buttons
// Buttons - Keep existing structure
buttons: {
primary: "inline-flex items-center justify-center font-bold text-white border-0 rounded-2xl transition-all duration-300 hover:shadow-xl hover:-translate-y-1 hover:scale-105 active:scale-95",
discord: "group relative overflow-hidden px-4 lg:px-6 py-2.5 lg:py-3 text-white font-semibold text-sm lg:text-base rounded-xl transition-all duration-300 hover:shadow-xl hover:shadow-indigo-500/25 hover:-translate-y-1 hover:scale-105",
auth: "group relative overflow-hidden px-4 lg:px-6 py-2.5 lg:py-3 text-white font-semibold text-sm lg:text-base rounded-xl transition-all duration-300 hover:shadow-xl hover:-translate-y-1 hover:scale-105"
},
// Cards
// Cards - Keep existing structure
cards: {
base: "backdrop-blur-lg rounded-2xl border border-banquise-blue-lightest/30 transition-all duration-300",
hover: "hover:shadow-xl hover:border-banquise-blue-lightest/50",
interactive: "cursor-pointer hover:-translate-y-4 hover:shadow-2xl active:scale-95"
},
// Text - Hiérarchie améliorée
// Text - Keep existing structure
text: {
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",
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",
// Sous-titres
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",
description: "text-banquise-gray/80 leading-relaxed",
muted: "text-banquise-gray/90 leading-relaxed",
// Texte sur fond sombre
lightHeading: "text-banquise-blue-lightest font-heading font-bold tracking-tight",
lightBody: "text-white/90 leading-relaxed"
},
// Layout
// Layout - Keep existing structure
layout: {
section: "py-12 sm:py-16 md:py-20 w-full max-w-6xl mx-auto px-4 sm:px-6 md:px-8",
container: "max-w-6xl mx-auto",
divider: "w-20 h-1 bg-gradient-to-r from-banquise-blue-lightest to-banquise-blue mx-auto mb-6 sm:mb-8 rounded-full"
},
// Icons and decorative elements
// Icons - Keep existing structure
icons: {
base: "w-16 h-16 sm:w-20 sm:h-20 lg:w-24 lg:h-24 rounded-2xl flex items-center justify-center text-3xl sm:text-4xl lg:text-5xl shadow-lg",
small: "w-10 h-10 rounded-lg flex items-center justify-center text-white"
},
// Navigation
// Navigation - Keep existing structure
nav: {
link: "px-4 lg:px-6 py-2.5 lg:py-3 text-white/90 hover:text-white font-medium text-sm lg:text-base rounded-xl transition-all duration-300 hover:bg-white/10 hover:backdrop-blur-sm relative group",
mobileItem: "group flex items-center p-4 text-white/90 hover:text-white no-underline rounded-2xl hover:bg-gradient-to-r hover:from-banquise-blue/20 hover:to-banquise-blue-light/20 transition-all duration-300 border border-transparent hover:border-banquise-blue-lightest/20"

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

@ -1,11 +1,5 @@
export interface Service {
name: string;
url: string;
image: string;
description: string;
features: string[];
icon: string;
}
// Re-export types from their specific modules
export type { Service } from './service';
export interface AccordionItemProps {
title: 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="react" />
/// <reference types="react-dom" />

View File

@ -34,6 +34,10 @@ export default {
'gentle-float': 'gentle-float 6s ease-in-out infinite',
'fadeIn': 'fadeIn 0.2s ease-out',
'slideUp': 'slideUp 0.3s ease-out',
'bubble-float': 'bubble-float 8s ease-in-out infinite',
'bubble-float-slow': 'bubble-float-slow 12s ease-in-out infinite',
'bubble-float-fast': 'bubble-float-fast 6s ease-in-out infinite',
'ocean-shimmer': 'ocean-shimmer 10s ease-in-out infinite',
},
keyframes: {
float: {
@ -92,6 +96,35 @@ export default {
from: { transform: 'translateY(30px)', opacity: '0' },
to: { transform: 'translateY(0)', opacity: '1' },
},
'bubble-float': {
'0%': { transform: 'translateY(0) translateX(0) scale(1)', opacity: '0.6' },
'25%': { transform: 'translateY(-15px) translateX(5px) scale(1.05)', opacity: '0.7' },
'50%': { transform: 'translateY(-30px) translateX(-3px) scale(1.1)', opacity: '0.5' },
'75%': { transform: 'translateY(-45px) translateX(8px) scale(1.05)', opacity: '0.4' },
'100%': { transform: 'translateY(-60px) translateX(0) scale(1)', opacity: '0.2' },
},
'bubble-float-slow': {
'0%': { transform: 'translateY(0) translateX(0) scale(0.8)', opacity: '0.4' },
'20%': { transform: 'translateY(-20px) translateX(-8px) scale(0.9)', opacity: '0.5' },
'40%': { transform: 'translateY(-40px) translateX(6px) scale(1.1)', opacity: '0.4' },
'60%': { transform: 'translateY(-60px) translateX(-4px) scale(1.2)', opacity: '0.3' },
'80%': { transform: 'translateY(-80px) translateX(10px) scale(1.0)', opacity: '0.2' },
'100%': { transform: 'translateY(-100px) translateX(0) scale(0.8)', opacity: '0.1' },
},
'bubble-float-fast': {
'0%': { transform: 'translateY(0) translateX(0) scale(1.2)', opacity: '0.8' },
'15%': { transform: 'translateY(-10px) translateX(4px) scale(1.1)', opacity: '0.7' },
'30%': { transform: 'translateY(-20px) translateX(-2px) scale(0.9)', opacity: '0.6' },
'45%': { transform: 'translateY(-30px) translateX(6px) scale(1.0)', opacity: '0.5' },
'60%': { transform: 'translateY(-40px) translateX(-5px) scale(1.1)', opacity: '0.4' },
'75%': { transform: 'translateY(-50px) translateX(3px) scale(1.0)', opacity: '0.3' },
'90%': { transform: 'translateY(-60px) translateX(7px) scale(0.9)', opacity: '0.2' },
'100%': { transform: 'translateY(-70px) translateX(0) scale(1.2)', opacity: '0.1' },
},
'ocean-shimmer': {
'0%, 100%': { opacity: '0.1', transform: 'translateX(-10px)' },
'50%': { opacity: '0.3', transform: 'translateX(10px)' },
},
},
backdropBlur: {
'xs': '2px',