Compare commits
28 Commits
main
...
arthur.wam
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bd42196f54 | ||
d3ec4524a7 | |||
5e42042077 | |||
![]() |
5b354d15a6 | ||
![]() |
3d0efdb15c | ||
![]() |
81dad2b6ba | ||
![]() |
34531fd2cf | ||
![]() |
b14490da1b | ||
![]() |
4ec4d6ccb2 | ||
![]() |
0f2d0fdb40 | ||
![]() |
ff5984a727 | ||
![]() |
978e46d0a7 | ||
![]() |
a563a04c65 | ||
![]() |
a6d054a09a | ||
![]() |
04d87e6113 | ||
![]() |
11c26e1ba9 | ||
![]() |
e327231b8a | ||
![]() |
cd6ef409f1 | ||
![]() |
80ee14b7da | ||
![]() |
870c9ac0d8 | ||
![]() |
bdf1124cf6 | ||
![]() |
ed775ee3e5 | ||
![]() |
549a95e797 | ||
![]() |
a5268d3714 | ||
![]() |
88d7694647 | ||
976e7af488 | |||
![]() |
6bebedb515 | ||
![]() |
65ea4ac0be |
31
.gitea/workflows/build.yaml
Normal file
31
.gitea/workflows/build.yaml
Normal file
@ -0,0 +1,31 @@
|
||||
name: Build
|
||||
run-name: CI/CD website
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
build-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.x'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd banquise-website
|
||||
npm ci
|
||||
- name: Building
|
||||
run: |
|
||||
cd banquise-website
|
||||
npm run build
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -30,7 +30,3 @@ pnpm-debug.log*
|
||||
|
||||
# optional: nix store link if using nix develop
|
||||
.result
|
||||
|
||||
# optional: lockfiles you don't use
|
||||
package-lock.json
|
||||
|
||||
|
5060
banquise-website/package-lock.json
generated
Normal file
5060
banquise-website/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,103 +1,144 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Navigation } from './components/layout/Navigation';
|
||||
import { Footer } from './components/layout/Footer';
|
||||
import React from 'react';
|
||||
import { ModernNavigation } from './components/layout/ModernNavigation';
|
||||
import { HeroSection } from './components/sections/HeroSection';
|
||||
import { TechFeaturesSection } from './components/sections/TechFeaturesSection';
|
||||
import { ServicesSection } from './components/sections/ServicesSection';
|
||||
import { TechFeaturesSection } from './components/sections/TechFeaturesSection';
|
||||
import { AboutSection } from './components/sections/AboutSection';
|
||||
import { Footer } from './components/layout/Footer';
|
||||
import { Popup } from './components/ui/Popup';
|
||||
import { ScrollToTopButton } from './components/ui/ScrollToTopButton';
|
||||
import { URLS } from './config/constants';
|
||||
|
||||
// Define Service interface directly in App
|
||||
interface Service {
|
||||
name: string;
|
||||
url: string;
|
||||
image: string;
|
||||
description: string;
|
||||
features: string[];
|
||||
icon: string;
|
||||
}
|
||||
import { ModernLanguageSwitcher } from './components/ui/ModernLanguageSwitcher';
|
||||
import { useTranslation } from './hooks/useTranslation';
|
||||
import { useServiceModal } from './hooks/useServiceModal';
|
||||
import { useAccordion } from './hooks/useAccordion';
|
||||
import { useOceanDepthEffect } from './hooks/useOceanDepthEffect';
|
||||
|
||||
const App: React.FC = () => {
|
||||
// Define services directly in the component with enhanced data
|
||||
const services: Service[] = [
|
||||
{
|
||||
name: "Wiki",
|
||||
url: URLS.services.wiki,
|
||||
image: "/src/assets/iceberg.png",
|
||||
icon: "📚",
|
||||
description: "Notre wiki collaboratif est votre centre de documentation technique. Accédez à des guides détaillés, des tutoriels et de la documentation API pour tous nos services.",
|
||||
features: [
|
||||
"Documentation collaborative en temps réel",
|
||||
"Guides d'installation détaillés",
|
||||
"API et références techniques",
|
||||
"Tutoriels pas à pas",
|
||||
"Base de connaissances communautaire",
|
||||
"Recherche avancée intégrée"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Gitea",
|
||||
url: URLS.services.gitea,
|
||||
image: "/src/assets/iceberg.png",
|
||||
icon: "🔧",
|
||||
description: "Instance Gitea auto-hébergée pour la gestion de vos dépôts Git. Collaborez sur vos projets avec un contrôle total sur votre code source.",
|
||||
features: [
|
||||
"Dépôts Git illimités",
|
||||
"Issues et pull requests",
|
||||
"Actions CI/CD intégrées",
|
||||
"Gestion d'équipes et permissions",
|
||||
"Interface web intuitive",
|
||||
"Intégration avec outils externes"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Panel",
|
||||
url: URLS.services.panel,
|
||||
image: "/src/assets/iceberg.png",
|
||||
icon: "🎮",
|
||||
description: "Interface de gestion centralisée pour vos serveurs de jeux. Déployez, configurez et surveillez vos serveurs gaming en quelques clics.",
|
||||
features: [
|
||||
"Déploiement automatisé de serveurs",
|
||||
"Monitoring en temps réel",
|
||||
"Gestion des ressources système",
|
||||
"Interface d'administration web",
|
||||
"Support multi-jeux",
|
||||
"Sauvegarde automatique des données"
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const [selectedService, setSelectedService] = useState<Service | null>(null);
|
||||
|
||||
// Inline accordion logic
|
||||
const [openAccordion, setOpenAccordion] = useState<string | null>(null);
|
||||
const toggleAccordion = (title: string) => {
|
||||
setOpenAccordion(openAccordion === title ? null : title);
|
||||
};
|
||||
const { t, currentLanguage, changeLanguage, availableLanguages } = useTranslation();
|
||||
const { selectedService, openServiceModal, closeServiceModal } = useServiceModal();
|
||||
const { openAccordion, toggleAccordion } = useAccordion();
|
||||
const scrollDepth = useOceanDepthEffect();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen w-full">
|
||||
<Navigation />
|
||||
<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 className="flex-1 flex flex-col overflow-x-hidden overflow-y-auto">
|
||||
<div className="relative flex-1 bg-ocean-gradient w-full min-h-screen flex flex-col justify-start items-center overflow-x-hidden">
|
||||
{/* 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>
|
||||
|
||||
<HeroSection />
|
||||
<TechFeaturesSection />
|
||||
<ServicesSection services={services} onServiceClick={setSelectedService} />
|
||||
<AboutSection openAccordion={openAccordion} toggleAccordion={toggleAccordion} />
|
||||
{/* 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>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
{/* 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>
|
||||
|
||||
{/* Bouton de retour en haut */}
|
||||
{/* Bulles supplémentaires pour un effet plus dense */}
|
||||
<div className="absolute top-1/8 left-1/2 w-1.5 h-1.5 bg-banquise-blue-lightest/25 rounded-full animate-bubble-float-fast delay-800"></div>
|
||||
<div className="absolute top-3/8 left-1/8 w-2 h-2 bg-banquise-blue-lightest/12 rounded-full animate-bubble-float delay-1200"></div>
|
||||
<div className="absolute top-5/8 left-7/8 w-1 h-1 bg-banquise-blue-lightest/20 rounded-full animate-bubble-float-slow delay-600"></div>
|
||||
<div className="absolute top-7/8 left-1/4 w-2.5 h-2.5 bg-banquise-blue-lightest/8 rounded-full animate-bubble-float-fast delay-1800"></div>
|
||||
<div className="absolute top-1/7 left-5/6 w-1 h-1 bg-banquise-blue-lightest/28 rounded-full animate-bubble-float delay-400"></div>
|
||||
<div className="absolute top-2/7 left-2/5 w-1.5 h-1.5 bg-banquise-blue-lightest/15 rounded-full animate-bubble-float-slow delay-900"></div>
|
||||
<div className="absolute top-4/7 left-3/8 w-2 h-2 bg-banquise-blue-lightest/10 rounded-full animate-bubble-float-fast delay-1400"></div>
|
||||
<div className="absolute top-6/7 left-4/5 w-1 h-1 bg-banquise-blue-lightest/18 rounded-full animate-bubble-float delay-200"></div>
|
||||
|
||||
{/* Bulles très petites pour densité */}
|
||||
<div className="absolute top-1/10 left-3/10 w-0.5 h-0.5 bg-banquise-blue-lightest/30 rounded-full animate-bubble-float-fast delay-100"></div>
|
||||
<div className="absolute top-3/10 left-7/10 w-0.5 h-0.5 bg-banquise-blue-lightest/25 rounded-full animate-bubble-float delay-1100"></div>
|
||||
<div className="absolute top-7/10 left-1/10 w-0.5 h-0.5 bg-banquise-blue-lightest/20 rounded-full animate-bubble-float-slow delay-1700"></div>
|
||||
<div className="absolute top-9/10 left-9/10 w-0.5 h-0.5 bg-banquise-blue-lightest/15 rounded-full animate-bubble-float-fast delay-2200"></div>
|
||||
|
||||
{/* Bulles moyennes pour variation */}
|
||||
<div className="absolute top-1/5 left-4/7 w-3 h-3 bg-banquise-blue-lightest/5 rounded-full animate-bubble-float-slow delay-1600"></div>
|
||||
<div className="absolute top-2/5 left-6/7 w-2.5 h-2.5 bg-banquise-blue-lightest/7 rounded-full animate-bubble-float delay-800"></div>
|
||||
<div className="absolute top-4/5 left-2/7 w-3.5 h-3.5 bg-banquise-blue-lightest/4 rounded-full animate-bubble-float-slow delay-2400"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contenu principal avec arrière-plan océanique uniforme */}
|
||||
<div className="relative z-10 text-white">
|
||||
{/* Navigation flottante */}
|
||||
<ModernNavigation
|
||||
translations={t.navigation}
|
||||
languageSwitcher={
|
||||
<ModernLanguageSwitcher
|
||||
currentLanguage={currentLanguage}
|
||||
onLanguageChange={changeLanguage}
|
||||
availableLanguages={availableLanguages}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Section Hero - Surface de l'océan */}
|
||||
<HeroSection translations={t.hero} />
|
||||
|
||||
{/* Section Services */}
|
||||
<ServicesSection
|
||||
services={t.services}
|
||||
onServiceClick={openServiceModal}
|
||||
translations={t.common}
|
||||
/>
|
||||
|
||||
{/* Section TechFeatures */}
|
||||
<TechFeaturesSection />
|
||||
|
||||
{/* Section About */}
|
||||
<AboutSection openAccordion={openAccordion} toggleAccordion={toggleAccordion} />
|
||||
|
||||
{/* Footer */}
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
{/* UI Components */}
|
||||
<ScrollToTopButton />
|
||||
|
||||
{selectedService && (
|
||||
<Popup service={selectedService} onClose={() => setSelectedService(null)} />
|
||||
<Popup
|
||||
service={selectedService}
|
||||
onClose={closeServiceModal}
|
||||
translations={t.common}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
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,65 +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>
|
||||
</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,14 +1,89 @@
|
||||
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 {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
translations: Translation['navigation'];
|
||||
}
|
||||
|
||||
export const MobileMenu: React.FC<MobileMenuProps> = ({ isOpen, onClose }) => {
|
||||
interface MobileNavItemProps {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
href: string;
|
||||
isExternal?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const MobileNavItem: React.FC<MobileNavItemProps> = ({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
href,
|
||||
isExternal = false,
|
||||
onClick
|
||||
}) => {
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
if (onClick) {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
onClick={handleClick}
|
||||
className={cn(
|
||||
'group flex items-center justify-between p-4 rounded-xl transition-all duration-300',
|
||||
'bg-white/5 hover:bg-white/10 active:bg-white/15',
|
||||
'border border-white/10 hover:border-white/20',
|
||||
'hover:scale-[1.02] active:scale-[0.98]',
|
||||
'hover:shadow-lg hover:shadow-banquise-blue/20'
|
||||
)}
|
||||
target={isExternal ? '_blank' : undefined}
|
||||
rel={isExternal ? 'noopener noreferrer' : undefined}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className={cn(
|
||||
'flex items-center justify-center w-10 h-10 rounded-xl',
|
||||
'bg-gradient-to-br from-banquise-blue-light/20 to-banquise-blue/20',
|
||||
'border border-banquise-blue-lightest/20',
|
||||
'group-hover:scale-110 transition-transform duration-300'
|
||||
)}>
|
||||
{icon}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<span className="block text-white font-semibold text-base group-hover:text-banquise-blue-lightest transition-colors">
|
||||
{title}
|
||||
</span>
|
||||
<p className="text-white/60 text-sm mt-0.5 group-hover:text-white/80 transition-colors">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Arrow Icon */}
|
||||
<div className={cn(
|
||||
'flex items-center justify-center w-6 h-6 rounded-full',
|
||||
'text-white/40 group-hover:text-white/80 transition-all duration-300',
|
||||
'group-hover:translate-x-1'
|
||||
)}>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export const MobileMenu: React.FC<MobileMenuProps> = ({ isOpen, onClose, translations }) => {
|
||||
// Gérer le scroll du body
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
@ -21,133 +96,171 @@ export const MobileMenu: React.FC<MobileMenuProps> = ({ isOpen, onClose }) => {
|
||||
};
|
||||
}, [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">Nos 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">À propos</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-.03z"/>
|
||||
</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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -3,8 +3,14 @@ import { MobileMenu } from './MobileMenu';
|
||||
import banquiseServer from '/src/assets/banquise_server.svg'
|
||||
import { URLS, SITE_CONFIG } from '../../config/constants';
|
||||
import { commonStyles } from '../../styles/components';
|
||||
import type { Translation } from '../../types/i18n';
|
||||
|
||||
export const Navigation: React.FC = () => {
|
||||
interface NavigationProps {
|
||||
translations: Translation['navigation'];
|
||||
languageSwitcher: React.ReactElement;
|
||||
}
|
||||
|
||||
export const Navigation: React.FC<NavigationProps> = ({ translations, languageSwitcher }) => {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
|
||||
@ -13,7 +19,6 @@ export const Navigation: React.FC = () => {
|
||||
const isScrolled = window.scrollY > 20;
|
||||
setScrolled(isScrolled);
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
@ -24,7 +29,6 @@ export const Navigation: React.FC = () => {
|
||||
setMobileMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
@ -38,7 +42,6 @@ export const Navigation: React.FC = () => {
|
||||
}`}>
|
||||
<div className={commonStyles.layout.container}>
|
||||
<div className="flex justify-between items-center h-16 sm:h-18 lg:h-20 px-4 sm:px-6 lg:px-8">
|
||||
|
||||
{/* Logo section */}
|
||||
<div className="flex items-center space-x-3 sm:space-x-4 group">
|
||||
<div className="relative">
|
||||
@ -62,19 +65,29 @@ export const Navigation: React.FC = () => {
|
||||
|
||||
{/* Navigation links desktop */}
|
||||
<div className="hidden md:flex items-center space-x-1 lg:space-x-2">
|
||||
<a href="#services" className={commonStyles.nav.link}>
|
||||
<span className="relative z-10">Services</span>
|
||||
<a href="#home" className={commonStyles.nav.link}>
|
||||
<span className="relative z-10">{translations.home}</span>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-banquise-blue-light/20 to-banquise-blue/20 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
</a>
|
||||
<a href="#services" className={commonStyles.nav.link}>
|
||||
<span className="relative z-10">{translations.services}</span>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-banquise-blue-light/20 to-banquise-blue/20 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
</a>
|
||||
|
||||
<a href="#about" className={commonStyles.nav.link}>
|
||||
<span className="relative z-10">À propos</span>
|
||||
<span className="relative z-10">{translations.about}</span>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-banquise-blue-light/20 to-banquise-blue/20 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
</a>
|
||||
<a href="#contact" className={commonStyles.nav.link}>
|
||||
<span className="relative z-10">{translations.contact}</span>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-banquise-blue-light/20 to-banquise-blue/20 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Action buttons desktop */}
|
||||
<div className="hidden md:flex items-center space-x-3 lg:space-x-4">
|
||||
{/* Language switcher */}
|
||||
{languageSwitcher}
|
||||
|
||||
<a
|
||||
href={URLS.social.discord}
|
||||
target="_blank"
|
||||
@ -89,7 +102,6 @@ export const Navigation: React.FC = () => {
|
||||
<span>Discord</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href={URLS.services.auth}
|
||||
target="_blank"
|
||||
@ -122,14 +134,13 @@ export const Navigation: React.FC = () => {
|
||||
aria-expanded={mobileMenuOpen}
|
||||
>
|
||||
<div className="w-6 h-6 relative">
|
||||
<span className={`absolute block w-6 h-0.5 bg-white transition-all duration-300 ${mobileMenuOpen ? 'rotate-45 top-3' : 'top-1'}`}></span>
|
||||
<span className={`absolute block w-6 h-6 bg-white transition-all duration-300 ${mobileMenuOpen ? 'rotate-45 top-3' : 'top-1'}`}></span>
|
||||
<span className={`absolute block w-6 h-0.5 bg-white transition-all duration-300 top-3 ${mobileMenuOpen ? 'opacity-0 scale-0' : 'opacity-100'}`}></span>
|
||||
<span className={`absolute block w-6 h-0.5 bg-white transition-all duration-300 ${mobileMenuOpen ? '-rotate-45 top-3' : 'top-5'}`}></span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Glassmorphism effect bar */}
|
||||
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-banquise-blue-lightest/30 to-transparent"></div>
|
||||
</nav>
|
||||
@ -141,6 +152,7 @@ export const Navigation: React.FC = () => {
|
||||
<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>
|
||||
@ -80,6 +79,38 @@ export const AboutSection: React.FC<AboutSectionProps> = ({ openAccordion, toggl
|
||||
<p className="text-banquise-gray/80 text-sm">Interface de gestion pour serveurs de jeux</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`flex items-start space-x-4 p-4 ${commonStyles.gradients.card} rounded-xl ${commonStyles.cards.base}`}>
|
||||
<div className={`${commonStyles.icons.small} ${commonStyles.gradients.primaryBr} font-bold`}>🐧</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-banquise-gray mb-1">Pelican</h4>
|
||||
<p className="text-banquise-gray/80 text-sm">Générateur de sites statiques</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`flex items-start space-x-4 p-4 ${commonStyles.gradients.card} rounded-xl ${commonStyles.cards.base}`}>
|
||||
<div className={`${commonStyles.icons.small} ${commonStyles.gradients.primaryBr} font-bold`}>🏢</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-banquise-gray mb-1">Intranet</h4>
|
||||
<p className="text-banquise-gray/80 text-sm">Espace privé de l'association</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`flex items-start space-x-4 p-4 ${commonStyles.gradients.card} rounded-xl ${commonStyles.cards.base}`}>
|
||||
<div className={`${commonStyles.icons.small} ${commonStyles.gradients.primaryBr} font-bold`}>📧</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-banquise-gray mb-1">Webmail</h4>
|
||||
<p className="text-banquise-gray/80 text-sm">Service de messagerie électronique</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`flex items-start space-x-4 p-4 ${commonStyles.gradients.card} rounded-xl ${commonStyles.cards.base}`}>
|
||||
<div className={`${commonStyles.icons.small} ${commonStyles.gradients.primaryBr} font-bold`}>☁️</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-banquise-gray mb-1">OpenCloud</h4>
|
||||
<p className="text-banquise-gray/80 text-sm">Plateforme cloud collaborative</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className={`${commonStyles.text.muted} mt-4`}>
|
||||
Tous nos services sont maintenus avec soin et régulièrement mis à jour pour garantir une expérience optimale.
|
||||
|
@ -1,8 +1,13 @@
|
||||
import React from 'react';
|
||||
import banquiseServer from '/src/assets/banquise_server.svg'
|
||||
import type { Translation } from '../../types/i18n';
|
||||
|
||||
export const HeroSection: React.FC = () => (
|
||||
<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">
|
||||
interface HeroSectionProps {
|
||||
translations: Translation['hero'];
|
||||
}
|
||||
|
||||
export const HeroSection: React.FC<HeroSectionProps> = ({ translations }) => (
|
||||
<section id="home" className="min-h-[calc(80vh-72px)] flex flex-col justify-center items-center text-center py-12 sm:py-16 md:py-20 w-full max-w-6xl mx-auto px-4 sm:px-6 md:px-8 relative z-3">
|
||||
<div className="mb-8 sm:mb-10 md:mb-12 w-32 h-32 sm:w-40 sm:h-40 md:w-48 md:h-48 rounded-full bg-gradient-to-br from-banquise-blue-dark/20 to-banquise-blue/10 p-4 sm:p-5 md:p-6 shadow-2xl backdrop-blur-sm border border-banquise-blue-lightest/30 relative group">
|
||||
<img
|
||||
src={banquiseServer}
|
||||
@ -15,15 +20,15 @@ export const HeroSection: React.FC = () => (
|
||||
</div>
|
||||
|
||||
<h1 className="text-banquise-gray text-3xl sm:text-4xl md:text-5xl lg:text-6xl mb-6 sm:mb-7 md:mb-8 font-extrabold leading-tight max-w-4xl font-heading px-2 relative z-10" style={{ textShadow: '0 2px 10px rgba(0, 0, 0, 0.3)' }}>
|
||||
Bienvenue sur La Banquise
|
||||
{translations.title}
|
||||
</h1>
|
||||
|
||||
<p className="text-banquise-gray text-lg sm:text-xl md:text-2xl mb-8 sm:mb-10 md:mb-12 max-w-3xl font-normal opacity-90 leading-relaxed px-2 relative z-10" style={{ textShadow: '0 1px 4px rgba(0, 0, 0, 0.2)' }}>
|
||||
Association d'hébergement et lab réseau pour tous les étudiants et associations de l'EPITA !
|
||||
{translations.subtitle}
|
||||
</p>
|
||||
|
||||
<a href="#services" className="inline-flex items-center justify-center bg-gradient-to-r from-banquise-gray to-white text-banquise-blue-dark border-0 py-4 sm:py-5 px-8 sm:px-10 md:px-12 rounded-2xl text-base sm:text-lg font-bold no-underline shadow-xl transition-all duration-300 min-w-48 sm:min-w-56 md:min-w-64 hover:-translate-y-2 hover:shadow-2xl hover:scale-105 backdrop-blur-sm border border-banquise-blue-lightest/20 mx-4 group relative z-10">
|
||||
<span className="text-center text-banquise-blue-dark">Découvrir nos services</span>
|
||||
<span className="text-center text-banquise-blue-dark">{translations.cta}</span>
|
||||
<span className="ml-2 sm:ml-3 text-lg sm:text-xl transition-transform duration-300 group-hover:translate-x-1 text-banquise-blue-dark">→</span>
|
||||
</a>
|
||||
</section>
|
||||
|
@ -1,22 +1,21 @@
|
||||
import React from 'react';
|
||||
import { ServiceCard } from '../common/ServiceCard';
|
||||
import { componentStyles } from '../../styles/designSystem';
|
||||
import type { Service } from '../../types/service';
|
||||
|
||||
// Declare the Service interface here
|
||||
interface Service {
|
||||
name: string;
|
||||
url: string;
|
||||
image: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
features: string[];
|
||||
}
|
||||
|
||||
// Define interface directly in the component file
|
||||
interface ServicesSectionProps {
|
||||
services: Service[];
|
||||
onServiceClick: (service: Service) => void;
|
||||
translations: {
|
||||
discoverFeatures: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const ServicesSection: React.FC<ServicesSectionProps> = ({ services, onServiceClick }) => (
|
||||
export const ServicesSection: React.FC<ServicesSectionProps> = ({
|
||||
services,
|
||||
onServiceClick,
|
||||
translations
|
||||
}) => (
|
||||
<section id="services" className="relative z-2 py-12 sm:py-16 md:py-20 w-full max-w-6xl mx-auto px-4 sm:px-6 md:px-8">
|
||||
<div className="w-20 h-1 bg-gradient-to-r from-banquise-blue-lightest to-banquise-blue mx-auto mb-6 sm:mb-8 rounded-full"></div>
|
||||
<h2 className="text-banquise-gray text-2xl sm:text-3xl md:text-4xl mb-4 sm:mb-6 text-center font-heading font-bold tracking-tight px-2" style={{ textShadow: '0 2px 4px rgba(0, 0, 0, 0.2)' }}>
|
||||
@ -28,35 +27,12 @@ export const ServicesSection: React.FC<ServicesSectionProps> = ({ services, onSe
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 sm:gap-8 w-full">
|
||||
{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">Découvrir toutes les fonctionnalités</span>
|
||||
<span className="ml-2 text-lg transition-transform duration-300 group-hover:translate-x-2">→</span>
|
||||
</div>
|
||||
|
||||
{/* Subtle hover effect */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-banquise-blue-lightest/10 to-banquise-blue/5 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none"></div>
|
||||
</div>
|
||||
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`}>
|
||||
|
37
banquise-website/src/components/ui/LanguageSwitcher.tsx
Normal file
37
banquise-website/src/components/ui/LanguageSwitcher.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import type { Language } from '../../types/i18n';
|
||||
|
||||
interface LanguageSwitcherProps {
|
||||
currentLanguage: Language;
|
||||
onLanguageChange: (language: Language) => void;
|
||||
availableLanguages: Language[];
|
||||
}
|
||||
|
||||
export const LanguageSwitcher: React.FC<LanguageSwitcherProps> = ({
|
||||
currentLanguage,
|
||||
onLanguageChange,
|
||||
availableLanguages
|
||||
}) => {
|
||||
const languageNames: Record<Language, string> = {
|
||||
fr: '🇫🇷 Français',
|
||||
en: '🇬🇧 English',
|
||||
//es: '🇪🇸 Español',
|
||||
//de: '🇩🇪 Deutsch'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative inline-block">
|
||||
<select
|
||||
value={currentLanguage}
|
||||
onChange={(e) => onLanguageChange(e.target.value as Language)}
|
||||
className="bg-banquise-blue-dark/20 border border-banquise-blue-lightest/30 text-banquise-gray rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-banquise-blue-light"
|
||||
>
|
||||
{availableLanguages.map((lang) => (
|
||||
<option key={lang} value={lang} className="bg-banquise-blue-dark text-banquise-gray">
|
||||
{languageNames[lang]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
};
|
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,21 +1,15 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { URLS } from '../../config/constants';
|
||||
|
||||
interface Service {
|
||||
name: string;
|
||||
url: string;
|
||||
image: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
features: string[];
|
||||
}
|
||||
import type { Service } from '../../types/service';
|
||||
import type { Translation } from '../../types/i18n';
|
||||
|
||||
interface PopupProps {
|
||||
service: Service;
|
||||
onClose: () => void;
|
||||
translations: Translation['common'];
|
||||
}
|
||||
|
||||
export const Popup: React.FC<PopupProps> = ({ service, onClose }) => {
|
||||
export const Popup: React.FC<PopupProps> = ({ service, onClose, translations }) => {
|
||||
// Empêcher le scroll du body quand la popup est ouverte
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = 'hidden';
|
||||
@ -34,7 +28,7 @@ export const Popup: React.FC<PopupProps> = ({ service, onClose }) => {
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="bg-white/90 hover:bg-white border border-banquise-blue/20 text-xl cursor-pointer text-banquise-blue-dark flex items-center justify-center w-10 h-10 sm:w-12 sm:h-12 rounded-full transition-all duration-200 hover:scale-110 active:scale-95 shadow-lg backdrop-blur-sm"
|
||||
aria-label="Fermer la popup"
|
||||
aria-label={translations.close}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@ -100,7 +94,7 @@ export const Popup: React.FC<PopupProps> = ({ service, onClose }) => {
|
||||
{/* Fonctionnalités */}
|
||||
<h3 className="text-xl sm:text-2xl lg:text-3xl mb-4 lg:mb-6 text-banquise-blue-dark font-heading font-bold flex items-center">
|
||||
<span className="text-xl sm:text-2xl lg:text-3xl mr-3">⚡</span>
|
||||
Fonctionnalités principales
|
||||
{translations.discoverFeatures}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
|
||||
{service.features.map((feature, index) => (
|
||||
|
@ -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
|
||||
|
@ -3,10 +3,15 @@ export const URLS = {
|
||||
wiki: "https://wiki.la-banquise.fr",
|
||||
gitea: "https://git.la-banquise.fr",
|
||||
panel: "https://panel.la-banquise.fr",
|
||||
auth: "https://auth.la-banquise.fr"
|
||||
auth: "https://auth.la-banquise.fr",
|
||||
pelican: "https://pelican.la-banquise.fr",
|
||||
intra: "https://intra.la-banquise.fr",
|
||||
mails: "https://mails.la-banquise.fr",
|
||||
opencloud: "https://opencloud.la-banquise.fr",
|
||||
ssp: "https://ssp.la-banquise.fr"
|
||||
},
|
||||
social: {
|
||||
discord: "https://discord.gg/labanquise"
|
||||
discord: "https://discord.gg/bJhM97wans"
|
||||
},
|
||||
contact: {
|
||||
email: "mailto:contact@la-banquise.fr"
|
||||
|
153
banquise-website/src/data/translations/en.ts
Normal file
153
banquise-website/src/data/translations/en.ts
Normal file
@ -0,0 +1,153 @@
|
||||
import type { Translation } from '../../types/i18n';
|
||||
import { URLS } from '../../config/constants';
|
||||
|
||||
export const en: Translation = {
|
||||
services: [
|
||||
{
|
||||
name: "Wiki",
|
||||
url: URLS.services.wiki,
|
||||
image: "/path/to/wiki-image.jpg",
|
||||
icon: "📚",
|
||||
description: "Collaborative technical documentation and knowledge sharing platform. Create, edit and organize your guides, tutorials and documentation as a team with integrated versioning system.",
|
||||
features: [
|
||||
"Advanced markdown editor with real-time preview",
|
||||
"Versioning system to track changes",
|
||||
"Real-time collaboration with multiple contributors",
|
||||
"Smart search across all documents",
|
||||
"Predefined templates for different documentation types",
|
||||
"Comment and revision system",
|
||||
"PDF and HTML export for external sharing",
|
||||
"Git integration for backup"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Gitea",
|
||||
url: URLS.services.gitea,
|
||||
image: "/path/to/gitea-image.jpg",
|
||||
icon: "🔧",
|
||||
description: "Lightweight and performant self-hosted Git service for your development projects. Open-source alternative to GitHub with all essential features for managing your repositories.",
|
||||
features: [
|
||||
"Unlimited public and private Git repositories",
|
||||
"Intuitive web interface for project management",
|
||||
"Issues and pull requests with review system",
|
||||
"Integrated wiki for each project",
|
||||
"CI/CD actions for automation",
|
||||
"Fine-grained permissions and team management",
|
||||
"Complete REST API for integration",
|
||||
"Webhooks for external notifications"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Gaming Panel",
|
||||
url: URLS.services.panel,
|
||||
image: "/path/to/panel-image.jpg",
|
||||
icon: "🎮",
|
||||
description: "Centralized management interface for all your game servers. Easily deploy, configure and monitor your Minecraft, CS2, Garry's Mod and many other servers.",
|
||||
features: [
|
||||
"Support for 20+ popular games (Minecraft, CS2, GMod...)",
|
||||
"One-click deployment with pre-configured templates",
|
||||
"Real-time administration console",
|
||||
"File management with integrated editor",
|
||||
"Performance and resource monitoring",
|
||||
"Automatic backup system",
|
||||
"Automated task scheduler"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Pelican",
|
||||
url: URLS.services.pelican,
|
||||
image: "/path/to/pelican-image.jpg",
|
||||
icon: "🐧",
|
||||
description: "Ultra-fast and flexible Python static site generator. Create blogs, portfolios or documentation sites with optimal performance and simplified deployment.",
|
||||
features: [
|
||||
"Game server management with dedicated servers (Minecraft, CS2, Palworld...)",
|
||||
"One-click deployment with pre-configured templates",
|
||||
"Real-time administration console",
|
||||
"File management with integrated editor",
|
||||
"Performance and resource monitoring",
|
||||
"Automatic backup system",
|
||||
"Automated task scheduler"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Intranet",
|
||||
url: URLS.services.intra,
|
||||
image: "/path/to/intra-image.jpg",
|
||||
icon: "🏢",
|
||||
description: "Secure private space for the association to centralize internal resources, communications and collaboration tools between members.",
|
||||
features: [
|
||||
"Personalized dashboard for each member",
|
||||
"Event and meeting calendar",
|
||||
"Secure file sharing",
|
||||
"Private discussion forums",
|
||||
"Member directory with profiles",
|
||||
"Internal notification system",
|
||||
"Project and task management",
|
||||
"Archive of decisions and minutes"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Mails",
|
||||
url: URLS.services.mails,
|
||||
image: "/path/to/mails-image.jpg",
|
||||
icon: "📧",
|
||||
description: "Professional email service with modern web interface. Benefit from a personalized @la-banquise.fr email address with all advanced features.",
|
||||
features: [
|
||||
"Personalized @la-banquise.fr email addresses",
|
||||
"Modern and responsive webmail interface",
|
||||
"Integrated anti-spam and antivirus filters",
|
||||
"Synchronized contacts and calendar",
|
||||
"IMAP/SMTP support for external clients",
|
||||
"Generous storage with archiving",
|
||||
"Communication encryption",
|
||||
"Automatic data backup"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Password Change",
|
||||
url: URLS.services.ssp,
|
||||
image: "/path/to/ssp-image.jpg",
|
||||
icon: "🔐",
|
||||
description: "Secure interface for autonomous password management. Easily change your credentials safely.",
|
||||
features: [
|
||||
"Secure interface to change your password",
|
||||
"Password complexity validation",
|
||||
"Email notifications of changes",
|
||||
"Modification history"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "OpenCloud",
|
||||
url: URLS.services.opencloud,
|
||||
image: "/path/to/opencloud-image.jpg",
|
||||
icon: "☁️",
|
||||
description: "Open-source collaborative cloud platform for file storage, sharing and synchronization. Free alternative to Google Drive with full control over your data.",
|
||||
features: [
|
||||
"Secure and encrypted cloud storage",
|
||||
"Multi-device synchronization",
|
||||
"File sharing with secure links",
|
||||
"Collaborative document editing",
|
||||
"Automatic file versioning",
|
||||
"Native mobile applications",
|
||||
"Integration with external tools",
|
||||
"Geo-redundant data backup"
|
||||
]
|
||||
}
|
||||
],
|
||||
hero: {
|
||||
title: "Welcome to La Banquise",
|
||||
subtitle: "Your trusted technology partner",
|
||||
cta: "Discover our services"
|
||||
},
|
||||
navigation: {
|
||||
home: "Home",
|
||||
services: "Services",
|
||||
about: "About",
|
||||
contact: "Contact"
|
||||
},
|
||||
common: {
|
||||
discoverFeatures: "Discover all features",
|
||||
close: "Close",
|
||||
loading: "Loading..."
|
||||
}
|
||||
};
|
153
banquise-website/src/data/translations/fr.ts
Normal file
153
banquise-website/src/data/translations/fr.ts
Normal file
@ -0,0 +1,153 @@
|
||||
import type { Translation } from '../../types/i18n';
|
||||
import { URLS } from '../../config/constants';
|
||||
|
||||
export const fr: Translation = {
|
||||
services: [
|
||||
{
|
||||
name: "Wiki",
|
||||
url: URLS.services.wiki,
|
||||
image: "/path/to/wiki-image.jpg",
|
||||
icon: "📚",
|
||||
description: "Plateforme collaborative de documentation technique et de partage de connaissances. Créez, modifiez et organisez vos guides, tutoriels et documentations en équipe avec un système de versioning intégré.",
|
||||
features: [
|
||||
"Éditeur markdown avancé avec prévisualisation en temps réel",
|
||||
"Système de versioning pour suivre les modifications",
|
||||
"Collaboration en temps réel avec plusieurs contributeurs",
|
||||
"Recherche intelligente dans tous les documents",
|
||||
"Templates prédéfinis pour différents types de documentation",
|
||||
"Système de commentaires et de révisions",
|
||||
"Export PDF et HTML pour partage externe",
|
||||
"Intégration avec Git pour la sauvegarde"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Gitea",
|
||||
url: URLS.services.gitea,
|
||||
image: "/path/to/gitea-image.jpg",
|
||||
icon: "🔧",
|
||||
description: "Service Git auto-hébergé lightweight et performant pour vos projets de développement. Alternative open-source à GitHub avec toutes les fonctionnalités essentielles pour gérer vos repositories.",
|
||||
features: [
|
||||
"Repositories Git illimités publics et privés",
|
||||
"Interface web intuitive pour la gestion des projets",
|
||||
"Issues et pull requests avec système de review",
|
||||
"Wiki intégré pour chaque projet",
|
||||
"Actions CI/CD pour l'automatisation",
|
||||
"Gestion fine des permissions et des équipes",
|
||||
"API REST complète pour l'intégration",
|
||||
"Webhooks pour les notifications externes"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Panel Gaming",
|
||||
url: URLS.services.panel,
|
||||
image: "/path/to/panel-image.jpg",
|
||||
icon: "🎮",
|
||||
description: "Interface de gestion centralisée pour tous vos serveurs de jeux. Déployez, configurez et surveillez facilement vos serveurs Minecraft, CS2, Garry's Mod et bien d'autres.",
|
||||
features: [
|
||||
"Support de 20+ jeux populaires (Minecraft, CS2, GMod...)",
|
||||
"Déploiement en un clic avec templates préconfigurés",
|
||||
"Console d'administration en temps réel",
|
||||
"Gestion des fichiers avec éditeur intégré",
|
||||
"Monitoring des performances et ressources",
|
||||
"Système de sauvegarde automatique",
|
||||
"Planificateur de tâches automatisées"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Pelican",
|
||||
url: URLS.services.pelican,
|
||||
image: "/path/to/pelican-image.jpg",
|
||||
icon: "🐧",
|
||||
description: "Générateur de sites statiques Python ultra-rapide et flexible. Créez des blogs, portfolios ou sites de documentation avec une performance optimale et un déploiement simplifié.",
|
||||
features: [
|
||||
"Gestion de serveurs de jeux avec serveurs dédiés (Minecraft, CS2, Palworld...)",
|
||||
"Déploiement en un clic avec templates préconfigurés",
|
||||
"Console d'administration en temps réel",
|
||||
"Gestion des fichiers avec éditeur intégré",
|
||||
"Monitoring des performances et ressources",
|
||||
"Système de sauvegarde automatique",
|
||||
"Planificateur de tâches automatisées"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Intranet",
|
||||
url: URLS.services.intra,
|
||||
image: "/path/to/intra-image.jpg",
|
||||
icon: "🏢",
|
||||
description: "Espace privé sécurisé de l'association pour centraliser les ressources internes, communications et outils de collaboration entre membres.",
|
||||
features: [
|
||||
"Tableau de bord personnalisé pour chaque membre",
|
||||
"Calendrier des événements et réunions",
|
||||
"Partage de fichiers sécurisé",
|
||||
"Forums de discussion privés",
|
||||
"Annuaire des membres avec profils",
|
||||
"Système de notifications internes",
|
||||
"Gestion des projets et tâches",
|
||||
"Archive des décisions et procès-verbaux"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Mails",
|
||||
url: URLS.services.mails,
|
||||
image: "/path/to/mails-image.jpg",
|
||||
icon: "📧",
|
||||
description: "Service de messagerie électronique professionnel avec interface web moderne. Bénéficiez d'une adresse email personnalisée @la-banquise.fr avec toutes les fonctionnalités avancées.",
|
||||
features: [
|
||||
"Adresses email personnalisées @la-banquise.fr",
|
||||
"Interface webmail moderne et responsive",
|
||||
"Filtres anti-spam et antivirus intégrés",
|
||||
"Contacts et calendrier synchronisés",
|
||||
"Support IMAP/SMTP pour clients externes",
|
||||
"Stockage généreux avec archivage",
|
||||
"Chiffrement des communications",
|
||||
"Sauvegarde automatique des données"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Changement de mot de passe",
|
||||
url: URLS.services.ssp,
|
||||
image: "/path/to/ssp-image.jpg",
|
||||
icon: "🔐",
|
||||
description: "Interface sécurisée pour la gestion autonome de vos mots de passe. Changez facilement vos identifiants en toute sécurité.",
|
||||
features: [
|
||||
"Interface sécurisée pour changer votre mot de passe",
|
||||
"Validation de la complexité des mots de passe",
|
||||
"Notifications par email des changements",
|
||||
"Historique des modifications"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "OpenCloud",
|
||||
url: URLS.services.opencloud,
|
||||
image: "/path/to/opencloud-image.jpg",
|
||||
icon: "☁️",
|
||||
description: "Plateforme cloud collaborative open-source pour le stockage, le partage et la synchronisation de fichiers. Alternative libre à Google Drive avec contrôle total sur vos données.",
|
||||
features: [
|
||||
"Stockage cloud sécurisé et chiffré",
|
||||
"Synchronisation multi-appareils",
|
||||
"Partage de fichiers avec liens sécurisés",
|
||||
"Édition collaborative de documents",
|
||||
"Versioning automatique des fichiers",
|
||||
"Applications mobiles natives",
|
||||
"Intégration avec outils externes",
|
||||
"Sauvegarde géoredondante des données"
|
||||
]
|
||||
}
|
||||
],
|
||||
hero: {
|
||||
title: "Bienvenue chez La Banquise",
|
||||
subtitle: "Votre partenaire technologique de confiance",
|
||||
cta: "Découvrir nos services"
|
||||
},
|
||||
navigation: {
|
||||
home: "Accueil",
|
||||
services: "Services",
|
||||
about: "À propos",
|
||||
contact: "Contact"
|
||||
},
|
||||
common: {
|
||||
discoverFeatures: "Découvrir toutes les fonctionnalités",
|
||||
close: "Fermer",
|
||||
loading: "Chargement..."
|
||||
}
|
||||
};
|
14
banquise-website/src/data/translations/index.ts
Normal file
14
banquise-website/src/data/translations/index.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { fr } from './fr';
|
||||
import { en } from './en';
|
||||
import type { Language, Translation } from '../../types/i18n';
|
||||
|
||||
export const translations: Record<Language, Translation> = {
|
||||
fr,
|
||||
en,
|
||||
// Ajoutez d'autres langues ici :
|
||||
// es,
|
||||
// de,
|
||||
};
|
||||
|
||||
export const defaultLanguage: Language = 'fr';
|
||||
export const availableLanguages: Language[] = ['fr', 'en'];
|
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,
|
||||
};
|
||||
};
|
34
banquise-website/src/hooks/useTranslation.tsx
Normal file
34
banquise-website/src/hooks/useTranslation.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import type { Language, Translation } from '../types/i18n';
|
||||
import { translations, defaultLanguage } from '../data/translations';
|
||||
|
||||
export const useTranslation = () => {
|
||||
const [currentLanguage, setCurrentLanguage] = useState<Language>(() => {
|
||||
// Récupérer la langue depuis localStorage ou utiliser la langue par défaut
|
||||
const saved = localStorage.getItem('language') as Language;
|
||||
return saved && translations[saved] ? saved : defaultLanguage;
|
||||
});
|
||||
|
||||
// Memoize the translation object to prevent unnecessary re-renders
|
||||
const t = useMemo<Translation>(() => translations[currentLanguage], [currentLanguage]);
|
||||
|
||||
// Memoize available languages array
|
||||
const availableLanguages = useMemo(() => Object.keys(translations) as Language[], []);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('language', currentLanguage);
|
||||
}, [currentLanguage]);
|
||||
|
||||
const changeLanguage = (language: Language) => {
|
||||
if (translations[language]) {
|
||||
setCurrentLanguage(language);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
t,
|
||||
currentLanguage,
|
||||
changeLanguage,
|
||||
availableLanguages
|
||||
};
|
||||
};
|
@ -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;
|
23
banquise-website/src/types/i18n.ts
Normal file
23
banquise-website/src/types/i18n.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import type { Service } from './service';
|
||||
|
||||
export interface Translation {
|
||||
services: Service[];
|
||||
hero: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
cta: string;
|
||||
};
|
||||
navigation: {
|
||||
home: string;
|
||||
services: string;
|
||||
about: string;
|
||||
contact: string;
|
||||
};
|
||||
common: {
|
||||
discoverFeatures: string;
|
||||
close: string;
|
||||
loading: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Language = 'fr' | 'en'; //| 'es' | 'de';
|
@ -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;
|
||||
|
8
banquise-website/src/types/service.ts
Normal file
8
banquise-website/src/types/service.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface Service {
|
||||
name: string;
|
||||
url: string;
|
||||
image: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
features: 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',
|
||||
|
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "website-front",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user