refactoring & UI improvment
Some checks failed
Build / build-check (pull_request) Failing after 26s

This commit is contained in:
sahamone 2025-07-10 18:35:45 +02:00
parent d3ec4524a7
commit bd42196f54
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 React from 'react';
import { Navigation } from './components/layout/Navigation'; import { ModernNavigation } from './components/layout/ModernNavigation';
import { HeroSection } from './components/sections/HeroSection'; import { HeroSection } from './components/sections/HeroSection';
import { ServicesSection } from './components/sections/ServicesSection'; import { ServicesSection } from './components/sections/ServicesSection';
import { TechFeaturesSection } from './components/sections/TechFeaturesSection'; import { TechFeaturesSection } from './components/sections/TechFeaturesSection';
@ -7,32 +7,102 @@ import { AboutSection } from './components/sections/AboutSection';
import { Footer } from './components/layout/Footer'; 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 { ParallaxBackground } from './components/ui/ParallaxBackground'; import { ModernLanguageSwitcher } from './components/ui/ModernLanguageSwitcher';
import { LanguageSwitcher } from './components/ui/LanguageSwitcher';
import { useTranslation } from './hooks/useTranslation'; 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 App: React.FC = () => {
const { t, currentLanguage, changeLanguage, availableLanguages } = useTranslation(); const { t, currentLanguage, changeLanguage, availableLanguages } = useTranslation();
const [selectedService, setSelectedService] = useState<Service | null>(null); const { selectedService, openServiceModal, closeServiceModal } = useServiceModal();
const [openAccordion, setOpenAccordion] = useState<string | null>(null); const { openAccordion, toggleAccordion } = useAccordion();
const scrollDepth = useOceanDepthEffect();
const toggleAccordion = (title: string) => {
setOpenAccordion(openAccordion === title ? null : title);
};
return ( 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"> <div className="min-h-screen relative overflow-x-hidden">
{/* Background Effects */} {/* Arrière-plan océanique uniforme avec assombrissement progressif basé sur le scroll */}
<ParallaxBackground /> <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 Content */} {/* Couche de profondeur progressive basée sur le scroll - Plus intense */}
<div className="relative z-10"> <div
{/* Navigation avec sélecteur de langue */} className="absolute inset-0 bg-gradient-to-b from-transparent via-banquise-blue-dark/60 to-banquise-blue-dark transition-opacity duration-500"
<Navigation style={{ opacity: scrollDepth * 0.9 }}
></div>
{/* Effet de profondeur supplémentaire pour les moyennes profondeurs - Plus sombre */}
<div
className="absolute inset-0 bg-gradient-to-b from-transparent via-banquise-blue-dark/70 to-banquise-blue-dark transition-opacity duration-500"
style={{ opacity: Math.max(0, (scrollDepth - 0.2) * 1.5) }}
></div>
{/* Assombrissement pour les grandes profondeurs - Plus intense */}
<div
className="absolute inset-0 bg-gradient-to-b from-transparent via-banquise-blue-dark/80 to-slate-900 transition-opacity duration-500"
style={{ opacity: Math.max(0, (scrollDepth - 0.5) * 2) }}
></div>
{/* Assombrissement final pour les abysses - Très sombre */}
<div
className="absolute inset-0 bg-gradient-to-b from-banquise-blue-dark/50 to-slate-900 transition-opacity duration-500"
style={{ opacity: Math.max(0, (scrollDepth - 0.7) * 2.5) }}
></div>
{/* Rayons de lumière subtils qui percent l'eau depuis la surface */}
<div className="absolute inset-0 bg-gradient-to-br from-banquise-blue-lightest/4 via-transparent to-transparent"></div>
<div className="absolute inset-0 bg-gradient-to-bl from-transparent via-banquise-blue-lightest/2 to-transparent"></div>
{/* Effet de scintillement subtil avec animations océaniques */}
<div className="absolute inset-0">
<div className="absolute top-0 left-1/4 w-px h-full bg-gradient-to-b from-banquise-blue-lightest/20 via-banquise-blue-lightest/8 to-transparent animate-ocean-shimmer"></div>
<div className="absolute top-0 left-2/3 w-px h-full bg-gradient-to-b from-banquise-blue-lightest/15 via-banquise-blue-lightest/6 to-transparent animate-ocean-shimmer delay-1000"></div>
<div className="absolute top-0 left-3/4 w-px h-full bg-gradient-to-b from-banquise-blue-lightest/10 via-banquise-blue-lightest/4 to-transparent animate-ocean-shimmer delay-2000"></div>
</div>
{/* Particules flottantes (bulles) pour l'effet sous-marin avec animations variées */}
<div className="absolute inset-0">
{/* Bulles réparties sur toute la hauteur pour l'effet océanique continu */}
<div className="absolute top-1/6 left-1/5 w-2 h-2 bg-banquise-blue-lightest/20 rounded-full animate-bubble-float"></div>
<div className="absolute top-1/4 left-3/4 w-1 h-1 bg-banquise-blue-lightest/15 rounded-full animate-bubble-float-fast delay-500"></div>
<div className="absolute top-1/3 left-1/3 w-3 h-3 bg-banquise-blue-lightest/10 rounded-full animate-bubble-float-slow"></div>
<div className="absolute top-1/2 left-4/5 w-1.5 h-1.5 bg-banquise-blue-lightest/18 rounded-full animate-bubble-float delay-1000"></div>
<div className="absolute top-2/3 left-1/6 w-2.5 h-2.5 bg-banquise-blue-lightest/8 rounded-full animate-bubble-float-slow delay-1500"></div>
<div className="absolute top-3/4 left-2/3 w-1 h-1 bg-banquise-blue-lightest/22 rounded-full animate-bubble-float-fast delay-700"></div>
<div className="absolute top-5/6 left-1/2 w-1.5 h-1.5 bg-banquise-blue-lightest/12 rounded-full animate-bubble-float delay-300"></div>
<div className="absolute top-11/12 left-3/5 w-2 h-2 bg-banquise-blue-lightest/6 rounded-full animate-bubble-float-slow delay-2000"></div>
{/* Bulles supplémentaires pour un effet plus dense */}
<div className="absolute top-1/8 left-1/2 w-1.5 h-1.5 bg-banquise-blue-lightest/25 rounded-full animate-bubble-float-fast delay-800"></div>
<div className="absolute top-3/8 left-1/8 w-2 h-2 bg-banquise-blue-lightest/12 rounded-full animate-bubble-float delay-1200"></div>
<div className="absolute top-5/8 left-7/8 w-1 h-1 bg-banquise-blue-lightest/20 rounded-full animate-bubble-float-slow delay-600"></div>
<div className="absolute top-7/8 left-1/4 w-2.5 h-2.5 bg-banquise-blue-lightest/8 rounded-full animate-bubble-float-fast delay-1800"></div>
<div className="absolute top-1/7 left-5/6 w-1 h-1 bg-banquise-blue-lightest/28 rounded-full animate-bubble-float delay-400"></div>
<div className="absolute top-2/7 left-2/5 w-1.5 h-1.5 bg-banquise-blue-lightest/15 rounded-full animate-bubble-float-slow delay-900"></div>
<div className="absolute top-4/7 left-3/8 w-2 h-2 bg-banquise-blue-lightest/10 rounded-full animate-bubble-float-fast delay-1400"></div>
<div className="absolute top-6/7 left-4/5 w-1 h-1 bg-banquise-blue-lightest/18 rounded-full animate-bubble-float delay-200"></div>
{/* Bulles très petites pour densité */}
<div className="absolute top-1/10 left-3/10 w-0.5 h-0.5 bg-banquise-blue-lightest/30 rounded-full animate-bubble-float-fast delay-100"></div>
<div className="absolute top-3/10 left-7/10 w-0.5 h-0.5 bg-banquise-blue-lightest/25 rounded-full animate-bubble-float delay-1100"></div>
<div className="absolute top-7/10 left-1/10 w-0.5 h-0.5 bg-banquise-blue-lightest/20 rounded-full animate-bubble-float-slow delay-1700"></div>
<div className="absolute top-9/10 left-9/10 w-0.5 h-0.5 bg-banquise-blue-lightest/15 rounded-full animate-bubble-float-fast delay-2200"></div>
{/* Bulles moyennes pour variation */}
<div className="absolute top-1/5 left-4/7 w-3 h-3 bg-banquise-blue-lightest/5 rounded-full animate-bubble-float-slow delay-1600"></div>
<div className="absolute top-2/5 left-6/7 w-2.5 h-2.5 bg-banquise-blue-lightest/7 rounded-full animate-bubble-float delay-800"></div>
<div className="absolute top-4/5 left-2/7 w-3.5 h-3.5 bg-banquise-blue-lightest/4 rounded-full animate-bubble-float-slow delay-2400"></div>
</div>
</div>
{/* Contenu principal avec arrière-plan océanique uniforme */}
<div className="relative z-10 text-white">
{/* Navigation flottante */}
<ModernNavigation
translations={t.navigation} translations={t.navigation}
languageSwitcher={ languageSwitcher={
<LanguageSwitcher <ModernLanguageSwitcher
currentLanguage={currentLanguage} currentLanguage={currentLanguage}
onLanguageChange={changeLanguage} onLanguageChange={changeLanguage}
availableLanguages={availableLanguages} availableLanguages={availableLanguages}
@ -40,16 +110,23 @@ const App: React.FC = () => {
} }
/> />
{/* Section Hero - Surface de l'océan */}
<HeroSection translations={t.hero} /> <HeroSection translations={t.hero} />
{/* Section Services */}
<ServicesSection <ServicesSection
services={t.services} services={t.services}
onServiceClick={setSelectedService} onServiceClick={openServiceModal}
translations={t.common} translations={t.common}
/> />
{/* Section TechFeatures */}
<TechFeaturesSection /> <TechFeaturesSection />
{/* Section About */}
<AboutSection openAccordion={openAccordion} toggleAccordion={toggleAccordion} /> <AboutSection openAccordion={openAccordion} toggleAccordion={toggleAccordion} />
{/* Footer */}
<Footer /> <Footer />
</div> </div>
@ -59,7 +136,7 @@ const App: React.FC = () => {
{selectedService && ( {selectedService && (
<Popup <Popup
service={selectedService} service={selectedService}
onClose={() => setSelectedService(null)} onClose={closeServiceModal}
translations={t.common} 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'; 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
<li> href={URLS.services.opencloud}
<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"> className="text-banquise-gray/80 hover:text-banquise-blue-lightest transition-colors duration-200"
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 OpenCloud
</a> </a>
</li>
</ul>
</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,7 +1,8 @@
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'; import type { Translation } from '../../types/i18n';
interface MobileMenuProps { interface MobileMenuProps {
@ -10,7 +11,79 @@ interface MobileMenuProps {
translations: Translation['navigation']; 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 }) => { 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';
@ -23,133 +96,171 @@ export const MobileMenu: React.FC<MobileMenuProps> = ({ isOpen, onClose, transla
}; };
}, [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">{translations.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">{translations.about}</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" />
<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> </svg>
}
title={translations.contact}
description="Nous envoyer un email"
href="mailto:contact@la-banquise.fr"
onClick={() => handleNavClick('contact')}
/>
</div> </div>
<div>
<span className="font-semibold text-lg">Discord</span> {/* Divider */}
<p className="text-white/60 text-sm">Rejoindre la communauté</p> <div className="border-t border-banquise-blue-lightest/20 my-6" />
</div>
</div> {/* Social & External Links */}
<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"> <div className="space-y-3">
<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" /> <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> </svg>
</a> }
title="Discord"
description="Rejoindre la communauté"
href={URLS.social.discord}
isExternal={true}
/>
</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

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

View File

@ -7,7 +7,7 @@ interface HeroSectionProps {
} }
export const HeroSection: React.FC<HeroSectionProps> = ({ translations }) => ( 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"> <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}

View File

@ -1,4 +1,6 @@
import React from 'react'; import React from 'react';
import { ServiceCard } from '../common/ServiceCard';
import { componentStyles } from '../../styles/designSystem';
import type { Service } from '../../types/service'; import type { Service } from '../../types/service';
interface ServicesSectionProps { 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"> <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">{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>
))} ))}
</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,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 = () => { 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

@ -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 type { Language, Translation } from '../types/i18n';
import { translations, defaultLanguage } from '../data/translations'; import { translations, defaultLanguage } from '../data/translations';
@ -9,10 +9,13 @@ export const useTranslation = () => {
return saved && translations[saved] ? saved : defaultLanguage; 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(() => { useEffect(() => {
setT(translations[currentLanguage]);
localStorage.setItem('language', currentLanguage); localStorage.setItem('language', currentLanguage);
}, [currentLanguage]); }, [currentLanguage]);
@ -26,6 +29,6 @@ export const useTranslation = () => {
t, t,
currentLanguage, currentLanguage,
changeLanguage, 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 = { 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

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