This commit is contained in:
parent
d3ec4524a7
commit
bd42196f54
853
banquise-website/package-lock.json
generated
853
banquise-website/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="relative z-10">
|
||||
{/* Navigation avec sélecteur de langue */}
|
||||
<Navigation
|
||||
{/* Couche de profondeur progressive basée sur le scroll - Plus intense */}
|
||||
<div
|
||||
className="absolute inset-0 bg-gradient-to-b from-transparent via-banquise-blue-dark/60 to-banquise-blue-dark transition-opacity duration-500"
|
||||
style={{ opacity: scrollDepth * 0.9 }}
|
||||
></div>
|
||||
|
||||
{/* Effet de profondeur supplémentaire pour les moyennes profondeurs - Plus sombre */}
|
||||
<div
|
||||
className="absolute inset-0 bg-gradient-to-b from-transparent via-banquise-blue-dark/70 to-banquise-blue-dark transition-opacity duration-500"
|
||||
style={{ opacity: Math.max(0, (scrollDepth - 0.2) * 1.5) }}
|
||||
></div>
|
||||
|
||||
{/* Assombrissement pour les grandes profondeurs - Plus intense */}
|
||||
<div
|
||||
className="absolute inset-0 bg-gradient-to-b from-transparent via-banquise-blue-dark/80 to-slate-900 transition-opacity duration-500"
|
||||
style={{ opacity: Math.max(0, (scrollDepth - 0.5) * 2) }}
|
||||
></div>
|
||||
|
||||
{/* Assombrissement final pour les abysses - Très sombre */}
|
||||
<div
|
||||
className="absolute inset-0 bg-gradient-to-b from-banquise-blue-dark/50 to-slate-900 transition-opacity duration-500"
|
||||
style={{ opacity: Math.max(0, (scrollDepth - 0.7) * 2.5) }}
|
||||
></div>
|
||||
|
||||
{/* Rayons de lumière subtils qui percent l'eau depuis la surface */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-banquise-blue-lightest/4 via-transparent to-transparent"></div>
|
||||
<div className="absolute inset-0 bg-gradient-to-bl from-transparent via-banquise-blue-lightest/2 to-transparent"></div>
|
||||
|
||||
{/* Effet de scintillement subtil avec animations océaniques */}
|
||||
<div className="absolute inset-0">
|
||||
<div className="absolute top-0 left-1/4 w-px h-full bg-gradient-to-b from-banquise-blue-lightest/20 via-banquise-blue-lightest/8 to-transparent animate-ocean-shimmer"></div>
|
||||
<div className="absolute top-0 left-2/3 w-px h-full bg-gradient-to-b from-banquise-blue-lightest/15 via-banquise-blue-lightest/6 to-transparent animate-ocean-shimmer delay-1000"></div>
|
||||
<div className="absolute top-0 left-3/4 w-px h-full bg-gradient-to-b from-banquise-blue-lightest/10 via-banquise-blue-lightest/4 to-transparent animate-ocean-shimmer delay-2000"></div>
|
||||
</div>
|
||||
|
||||
{/* Particules flottantes (bulles) pour l'effet sous-marin avec animations variées */}
|
||||
<div className="absolute inset-0">
|
||||
{/* Bulles réparties sur toute la hauteur pour l'effet océanique continu */}
|
||||
<div className="absolute top-1/6 left-1/5 w-2 h-2 bg-banquise-blue-lightest/20 rounded-full animate-bubble-float"></div>
|
||||
<div className="absolute top-1/4 left-3/4 w-1 h-1 bg-banquise-blue-lightest/15 rounded-full animate-bubble-float-fast delay-500"></div>
|
||||
<div className="absolute top-1/3 left-1/3 w-3 h-3 bg-banquise-blue-lightest/10 rounded-full animate-bubble-float-slow"></div>
|
||||
<div className="absolute top-1/2 left-4/5 w-1.5 h-1.5 bg-banquise-blue-lightest/18 rounded-full animate-bubble-float delay-1000"></div>
|
||||
<div className="absolute top-2/3 left-1/6 w-2.5 h-2.5 bg-banquise-blue-lightest/8 rounded-full animate-bubble-float-slow delay-1500"></div>
|
||||
<div className="absolute top-3/4 left-2/3 w-1 h-1 bg-banquise-blue-lightest/22 rounded-full animate-bubble-float-fast delay-700"></div>
|
||||
<div className="absolute top-5/6 left-1/2 w-1.5 h-1.5 bg-banquise-blue-lightest/12 rounded-full animate-bubble-float delay-300"></div>
|
||||
<div className="absolute top-11/12 left-3/5 w-2 h-2 bg-banquise-blue-lightest/6 rounded-full animate-bubble-float-slow delay-2000"></div>
|
||||
|
||||
{/* Bulles supplémentaires pour un effet plus dense */}
|
||||
<div className="absolute top-1/8 left-1/2 w-1.5 h-1.5 bg-banquise-blue-lightest/25 rounded-full animate-bubble-float-fast delay-800"></div>
|
||||
<div className="absolute top-3/8 left-1/8 w-2 h-2 bg-banquise-blue-lightest/12 rounded-full animate-bubble-float delay-1200"></div>
|
||||
<div className="absolute top-5/8 left-7/8 w-1 h-1 bg-banquise-blue-lightest/20 rounded-full animate-bubble-float-slow delay-600"></div>
|
||||
<div className="absolute top-7/8 left-1/4 w-2.5 h-2.5 bg-banquise-blue-lightest/8 rounded-full animate-bubble-float-fast delay-1800"></div>
|
||||
<div className="absolute top-1/7 left-5/6 w-1 h-1 bg-banquise-blue-lightest/28 rounded-full animate-bubble-float delay-400"></div>
|
||||
<div className="absolute top-2/7 left-2/5 w-1.5 h-1.5 bg-banquise-blue-lightest/15 rounded-full animate-bubble-float-slow delay-900"></div>
|
||||
<div className="absolute top-4/7 left-3/8 w-2 h-2 bg-banquise-blue-lightest/10 rounded-full animate-bubble-float-fast delay-1400"></div>
|
||||
<div className="absolute top-6/7 left-4/5 w-1 h-1 bg-banquise-blue-lightest/18 rounded-full animate-bubble-float delay-200"></div>
|
||||
|
||||
{/* Bulles très petites pour densité */}
|
||||
<div className="absolute top-1/10 left-3/10 w-0.5 h-0.5 bg-banquise-blue-lightest/30 rounded-full animate-bubble-float-fast delay-100"></div>
|
||||
<div className="absolute top-3/10 left-7/10 w-0.5 h-0.5 bg-banquise-blue-lightest/25 rounded-full animate-bubble-float delay-1100"></div>
|
||||
<div className="absolute top-7/10 left-1/10 w-0.5 h-0.5 bg-banquise-blue-lightest/20 rounded-full animate-bubble-float-slow delay-1700"></div>
|
||||
<div className="absolute top-9/10 left-9/10 w-0.5 h-0.5 bg-banquise-blue-lightest/15 rounded-full animate-bubble-float-fast delay-2200"></div>
|
||||
|
||||
{/* Bulles moyennes pour variation */}
|
||||
<div className="absolute top-1/5 left-4/7 w-3 h-3 bg-banquise-blue-lightest/5 rounded-full animate-bubble-float-slow delay-1600"></div>
|
||||
<div className="absolute top-2/5 left-6/7 w-2.5 h-2.5 bg-banquise-blue-lightest/7 rounded-full animate-bubble-float delay-800"></div>
|
||||
<div className="absolute top-4/5 left-2/7 w-3.5 h-3.5 bg-banquise-blue-lightest/4 rounded-full animate-bubble-float-slow delay-2400"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contenu principal avec arrière-plan océanique uniforme */}
|
||||
<div className="relative z-10 text-white">
|
||||
{/* Navigation flottante */}
|
||||
<ModernNavigation
|
||||
translations={t.navigation}
|
||||
languageSwitcher={
|
||||
<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}
|
||||
/>
|
||||
)}
|
||||
|
65
banquise-website/src/components/common/Button.tsx
Normal file
65
banquise-website/src/components/common/Button.tsx
Normal 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>
|
||||
);
|
||||
};
|
40
banquise-website/src/components/common/Card.tsx
Normal file
40
banquise-website/src/components/common/Card.tsx
Normal 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>
|
||||
);
|
||||
};
|
57
banquise-website/src/components/common/ServiceCard.tsx
Normal file
57
banquise-website/src/components/common/ServiceCard.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
120
banquise-website/src/components/layout/ModernNavigation.tsx
Normal file
120
banquise-website/src/components/layout/ModernNavigation.tsx
Normal 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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
52
banquise-website/src/components/layout/navbar/Logo.tsx
Normal file
52
banquise-website/src/components/layout/navbar/Logo.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
174
banquise-website/src/components/layout/navbar/NavLinks.tsx
Normal file
174
banquise-website/src/components/layout/navbar/NavLinks.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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`}>
|
||||
|
110
banquise-website/src/components/ui/ModernLanguageSwitcher.tsx
Normal file
110
banquise-website/src/components/ui/ModernLanguageSwitcher.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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
|
||||
|
33
banquise-website/src/hooks/useAccordion.ts
Normal file
33
banquise-website/src/hooks/useAccordion.ts
Normal 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,
|
||||
};
|
||||
};
|
22
banquise-website/src/hooks/useOceanDepthEffect.ts
Normal file
22
banquise-website/src/hooks/useOceanDepthEffect.ts
Normal 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;
|
||||
};
|
49
banquise-website/src/hooks/useScrollEffects.ts
Normal file
49
banquise-website/src/hooks/useScrollEffects.ts
Normal 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,
|
||||
};
|
||||
};
|
27
banquise-website/src/hooks/useServiceModal.ts
Normal file
27
banquise-website/src/hooks/useServiceModal.ts
Normal 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,
|
||||
};
|
||||
};
|
@ -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
|
||||
};
|
||||
};
|
||||
|
@ -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"
|
||||
|
176
banquise-website/src/styles/designSystem.ts
Normal file
176
banquise-website/src/styles/designSystem.ts
Normal 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;
|
@ -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;
|
||||
|
30
banquise-website/src/utils/classNames.ts
Normal file
30
banquise-website/src/utils/classNames.ts
Normal 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] : '');
|
||||
};
|
2
banquise-website/src/vite-env.d.ts
vendored
2
banquise-website/src/vite-env.d.ts
vendored
@ -1 +1,3 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="react" />
|
||||
/// <reference types="react-dom" />
|
||||
|
@ -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',
|
||||
|
Loading…
x
Reference in New Issue
Block a user