2025-05-31 19:17:55 +02:00

523 lines
20 KiB
TypeScript

import { FiUser, FiDatabase, FiShield, FiChevronDown } from 'react-icons/fi'
import { FaDiscord, FaArrowRight, FaEnvelope, FaGithub, FaNetworkWired, FaServer, FaLaptopCode, FaCloudUploadAlt, FaExternalLinkAlt } from 'react-icons/fa'
import { FiX, FiExternalLink } from 'react-icons/fi'
import './App.css'
import icebergImage from './assets/iceberg.png'
import logoImage from './assets/banquise_server.svg'
import { useEffect, useState, useMemo, useCallback, useRef } from 'react'
import aboutImage from './assets/banquise.png'
function App() {
const [selectedService, setSelectedService] = useState<number | null>(null);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const mobileMenuRef = useRef<HTMLDivElement>(null);
const services = useMemo(() => [
{
name: "Wiki",
url: "https://wiki.la-banquise.fr/",
description: "Une instance de wikijs, ou nous essayons de documenter nos projets, nos services ou encore notre infra, et aussi des petits tutoriels pour bien comprendre les outils utilises a EPITA !"
},
{
name: "Git",
url: "https://git.la-banquise.fr/",
description: "Gitea est notre plateforme de gestion de code source, similaire à GitHub, hébergée par nos soins. Nos divers projets necessitant Git, comme par exemple ce site, sont heberges et développes grace a cet outil."
},
{
name: "Panel jeux",
url: "https://panel.la-banquise.fr/auth/login",
description: "Interface de connection à notre panel Pterodactyl, qui vous permet de gérer vos serveurs de jeux. Celui ci sera remplace dans l ete par pelican."
},
], []);
const [icebergs, setIcebergs] = useState<Array<{
id: number,
x: number,
y: number,
scale: number,
rotation: number,
service: typeof services[0],
floatClass: string
}>>([])
const [reducedMotion, setReducedMotion] = useState(false);
useEffect(() => {
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
setReducedMotion(prefersReducedMotion);
const startTime = performance.now();
let count = 0;
while (performance.now() - startTime < 5) {
count++;
}
if (count < 1000) {
setReducedMotion(true);
}
}, []);
const positionIcebergs = useCallback(() => {
const newIcebergs = [];
const positions = [
{ x: 25, y: 35, scale: 0.95, rotation: 0 },
{ x: 50, y: 25, scale: 1.1, rotation: 0 },
{ x: 75, y: 35, scale: 0.95, rotation: 0 },
];
const floatClasses = ['float-1', 'float-2', 'float-3', 'float-4', 'float-5'];
for (let i = 0; i < services.length; i++) {
const position = positions[i % positions.length];
newIcebergs.push({
id: i,
x: position.x,
y: position.y,
scale: position.scale,
rotation: position.rotation,
service: services[i],
floatClass: reducedMotion ? '' : floatClasses[i % floatClasses.length]
});
}
return newIcebergs;
}, [services, reducedMotion]);
useEffect(() => {
setIcebergs(positionIcebergs());
}, [positionIcebergs]);
const renderBubbles = useMemo(() => {
if (reducedMotion) return null;
return (
<div className="bubbles">
<div className="bubble"></div>
<div className="bubble"></div>
<div className="bubble"></div>
<div className="bubble"></div>
<div className="bubble"></div>
<div className="bubble"></div>
<div className="bubble"></div>
</div>
);
}, [reducedMotion]);
const handleIcebergClick = (event: React.MouseEvent, serviceId: number) => {
event.preventDefault();
setSelectedService(serviceId);
};
const handleClosePopup = () => {
setSelectedService(null);
};
useEffect(() => {
const handleOutsideClick = (event: MouseEvent) => {
if (mobileMenuRef.current && !mobileMenuRef.current.contains(event.target as Node)) {
setMobileMenuOpen(false);
}
};
if (mobileMenuOpen) {
document.addEventListener('mousedown', handleOutsideClick);
}
return () => {
document.removeEventListener('mousedown', handleOutsideClick);
};
}, [mobileMenuOpen]);
useEffect(() => {
if (mobileMenuOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'auto';
}
return () => {
document.body.style.overflow = 'auto';
};
}, [mobileMenuOpen]);
const [activeAccordion, setActiveAccordion] = useState<number | null>(null);
// FAQ items for the about section
const faqItems = useMemo(() => [
{
question: "Qui sommes-nous ?",
answer: (
<>
<p>La Banquise est une association étudiante de l'EPITA, dont l'objectif est de former les epiteens a diverses notions de reseau et d'hebergement de servcies.
Fondée a la rentree 2022, notre asso permet à ceux qui le souhaitent de se former sur des technologies d'hébergement et de réseau, sur nos serveurs, a dispositon des etudiants.</p>
<p>Notre équipe est composée d'étudiants passionnés par l'informatique, le réseau et le partage de connaissances. Nous mettons notre expertise au service des étudiants et des associations de l'EPITA.</p>
</>
)
},
{
question: "Comment candidater pour rejoindre La Banquise ?",
answer: (
<>
<p>Pour nous rejoindre, rien de plus simple :</p>
<br/>
<ul>
<li>Rejoignez notre serveur Discord</li>
<li>Donnez votre login EPITA dans un ticket, ou expliquer votre situation/besoins.</li>
<li>Un moderateur vous mettra le role necessaire pour acceder au salons avec tout nos projets sur discord !</li>
</ul>
<br/>
<p>Si vous etes motivé.e, peu importe votre niveau technique actuel, n'hesitez pas a venir nous poser des question ou venir demander des ressources pour un projet !</p>
<a href="https://discord.com/invite/QQWwzX5ptY" className="accordion-cta" target="_blank" rel="noopener noreferrer">
Rejoindre notre Discord <FaExternalLinkAlt className="accordion-cta-icon" />
</a>
</>
)
},
{
question: "Quels sont nos objectifs ?",
answer: (
<>
<p>Nos principaux objectifs sont :</p>
<ul>
<li>Former les étudiants aux technologies d'hébergement et de réseau</li>
<li>Fournir des services informatiques de qualité aux étudiants et associations de l'EPITA</li>
<li>Promouvoir le partage de connaissances et l'entraide</li>
<li>Créer un environnement d'apprentissage pratique par l'expérience</li>
<li>Maintenir une infrastructure robuste et sécurisée</li>
</ul>
</>
)
},
{
question: "Comment utiliser nos services ?",
answer: (
<>
<p>Pour accéder à nos services :</p>
<ol>
<li>Connectez-vous au service souhaité avec vos identifiants, pour le moment crees par un admin</li>
<li>Consultez notre Wiki pour obtenir de l'aide sur l'utilisation de chaque service !</li>
</ol>
<p>Si vous rencontrez des difficultés, n'hésitez pas à demander de l'aide sur notre Discord.</p>
<a href="https://auth.la-banquise.fr/" className="accordion-cta" target="_blank" rel="noopener noreferrer">
Se Connecter <FaExternalLinkAlt className="accordion-cta-icon" />
</a>
</>
)
},
{
question: "Comment contribuer a un projet ?",
answer: (
<>
<p>Il existe plusieurs façons de contribuer aux projets La Banquise :</p>
<ul>
<li>Deployer des services</li>
<li>Experimenter et documenter le fonctionnement de technologies (k8s, openstack...)</li>
<li>Aider a gerer les ressources de l'asso</li>
<li>Contribuer au code source de nos projets via Gitea</li>
<li>Rédiger ou améliorer la documentation sur notre Wiki</li>
<li>Proposer de nouvelles idées de services ou d'améliorations</li>
<li>Aider d'autres utilisateurs sur notre Discord</li>
</ul>
<p>Toutes les contributions sont les bienvenues, même les plus modestes !</p>
</>
)
},
], []);
const toggleAccordion = (index: number) => {
setActiveAccordion(activeAccordion === index ? null : index);
};
return (
<div className="app-container">
<a href="#main-content" className="sr-only focus:not-sr-only">Passer au contenu principal</a>
<header>
<nav className="navbar" aria-label="Navigation principale">
<div className="navbar-left">
<img src={logoImage} alt="Logo La Banquise" className="site-logo" />
<h1 className="site-name">La Banquise</h1>
</div>
<button
className={`navbar-mobile-toggle ${mobileMenuOpen ? 'active' : ''}`}
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-expanded={mobileMenuOpen}
aria-label="Menu de navigation"
>
<span></span>
<span></span>
<span></span>
</button>
<div
className={`navbar-right ${mobileMenuOpen ? 'mobile-active' : ''}`}
ref={mobileMenuRef}
>
<a
href="https://discord.com/invite/QQWwzX5ptY"
className="discord-button"
target="_blank"
rel="noopener noreferrer"
aria-label="Rejoindre notre Discord"
onClick={() => setMobileMenuOpen(false)}
>
<FaDiscord className="discord-icon" aria-hidden="true" />
<span>Discord</span>
</a>
<a
href="https://auth.la-banquise.fr/"
className="login-button"
aria-label="Se connecter à votre compte"
onClick={() => setMobileMenuOpen(false)}
>
<FiUser className="login-icon" aria-hidden="true" />
<span>Se connecter</span>
</a>
</div>
<div
className={`mobile-menu-overlay ${mobileMenuOpen ? 'active' : ''}`}
onClick={() => setMobileMenuOpen(false)}
></div>
</nav>
</header>
<main id="main-content" className="content">
<div className="ocean" role="region" aria-label="Services La Banquise">
{renderBubbles}
<section className="page-section hero-section">
<div className="hero-tech-elements">
<div className="tech-element tech-element-1"><FaServer /></div>
<div className="tech-element tech-element-2"><FaLaptopCode /></div>
<div className="tech-element tech-element-3"><FaNetworkWired /></div>
<div className="tech-element tech-element-4"><FaCloudUploadAlt /></div>
</div>
<div className="hero-logo-container">
<img src={logoImage} alt="Logo La Banquise" className="hero-logo" />
</div>
<h2 className="hero-title">Association La Banquise</h2>
<p className="hero-subtitle">
Association d'hébergement et lab réseau pour tous les étudiants et associations de l'EPITA !
</p>
<div>
<a href="https://discord.com/invite/QQWwzX5ptY" className="cta-button" target="_blank" rel="noopener noreferrer">
Notre Discord
<FaArrowRight className="cta-icon" />
</a>
</div>
</section>
<section className="page-section tech-features-section">
<div className="section-divider"></div>
<h2 className="section-title">Notre infrastructure</h2>
<p className="section-subtitle">
25+ serveurs pour répondre à vos besoins
</p>
<p className="section-subtitle">
Un local a EPITA Lyon, prochainement a Paris ?
</p>
<div className="tech-features-grid">
<div className="tech-feature-card">
<div className="tech-feature-icon">
<FaServer />
</div>
<h3 className="tech-feature-title">Serveurs performants</h3>
<p className="tech-feature-description">
Infrastructure optimisée pour assurer des performances élevées et une disponibilité maximale de vos applications
</p>
</div>
<div className="tech-feature-card">
<div className="tech-feature-icon">
<FiDatabase />
</div>
<h3 className="tech-feature-title">Stockage sécurisé</h3>
<p className="tech-feature-description">
Solutions de stockage distribuées avec redondance pour garantir l'intégrité et la durabilité de vos données
</p>
</div>
<div className="tech-feature-card">
<div className="tech-feature-icon">
<FaNetworkWired />
</div>
<h3 className="tech-feature-title">Réseau optimisé</h3>
<p className="tech-feature-description">
Architecture réseau à haute disponibilité avec une faible latence pour vos applications critiques
</p>
</div>
<div className="tech-feature-card">
<div className="tech-feature-icon">
<FiShield />
</div>
<h3 className="tech-feature-title">Sécurité renforcée</h3>
<p className="tech-feature-description">
Protection contre les menaces avec systèmes de sécurité modernes et mises à jour régulières
</p>
</div>
</div>
</section>
<section className="page-section services-section" id="services">
<div className="section-divider"></div>
<h2 className="section-title">Nos services</h2>
<p className="section-subtitle">
Explorez notre écosystème de services conçus pour répondre à vos besoins.
</p>
<div
className="icebergs-container"
role="list"
aria-label="Liste des services disponibles"
>
{icebergs.map((iceberg) => (
<a
key={iceberg.id}
href={iceberg.service.url}
className={`iceberg ${iceberg.floatClass}`}
style={{
transform: `rotate(${iceberg.rotation}deg) scale(${iceberg.scale}) translateZ(0)`,
}}
role="listitem"
aria-label={`En savoir plus sur ${iceberg.service.name}`}
onClick={(e) => handleIcebergClick(e, iceberg.id)}
>
<img
src={icebergImage}
alt=""
className="iceberg-image"
loading="lazy"
aria-hidden="true"
/>
<div className="service-name">{iceberg.service.name}</div>
</a>
))}
</div>
</section>
<section className="page-section about-section" id="about">
<div className="section-divider"></div>
<h2 className="section-title">À propos de nous</h2>
<p className="section-subtitle">
Découvrez notre mission et posez-nous vos questions
</p>
<div className="about-container">
<div className="about-image-container">
<img src={aboutImage} alt="Logo La Banquise" className="about-logo" />
</div>
<p className="about-intro">
La Banquise est une association étudiante dédiée à l'hébergement de services et à la formation sur les technologies réseau, au service de la communauté EPITA.
</p>
<div className="accordion-group" role="region" aria-label="Foire aux questions">
{faqItems.map((item, index) => (
<div
key={index}
className={`accordion-item ${activeAccordion === index ? 'active' : ''}`}
>
<div
className="accordion-header"
onClick={() => toggleAccordion(index)}
role="button"
aria-expanded={activeAccordion === index}
aria-controls={`accordion-content-${index}`}
tabIndex={0}
onKeyPress={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
toggleAccordion(index);
}
}}
>
{item.question}
<FiChevronDown className="accordion-toggle-icon" aria-hidden="true" />
</div>
<div
className="accordion-content"
id={`accordion-content-${index}`}
role="region"
>
{item.answer}
</div>
</div>
))}
</div>
</div>
</section>
{selectedService !== null && (
<div className="popup-overlay" onClick={handleClosePopup} role="dialog" aria-modal="true" aria-labelledby="popup-title">
<div className="popup-content" onClick={(e) => e.stopPropagation()}>
<button className="popup-close" onClick={handleClosePopup} aria-label="Fermer">
<FiX />
</button>
<h3 id="popup-title" className="popup-title">{services[selectedService].name}</h3>
<p className="popup-description">{services[selectedService].description}</p>
<a
href={services[selectedService].url}
className="popup-button"
target="_blank"
rel="noopener noreferrer"
>
<FiExternalLink className="popup-button-icon" />
<span>Accéder au service</span>
</a>
</div>
</div>
)}
<div className="waves" aria-hidden="true">
<div className="wave wave1"></div>
<div className="wave wave2"></div>
<div className="wave wave3"></div>
</div>
</div>
</main>
<footer className="footer">
<div className="footer-content">
<div className="footer-column">
<h4>La Banquise</h4>
<ul>
<li><a href="#about">À propos</a></li>
<li><a href="#services">Services</a></li>
<li><a href="https://wiki.la-banquise.fr/en/home">Documentation</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
</div>
<div className="footer-column">
<h4>Services</h4>
<ul>
{services.map((service, index) => (
<li key={index}><a href={service.url}>{service.name}</a></li>
))}
</ul>
</div>
<div className="footer-column">
<h4>Communauté</h4>
<ul>
<li><a href="https://discord.com/invite/QQWwzX5ptY" target="_blank" rel="noopener noreferrer">Discord</a></li>
<li><a href="https://git.la-banquise.fr/" target="_blank" rel="noopener noreferrer">Gitea</a></li>
<li><a href="mailto:contact@la-banquise.fr"><FaEnvelope style={{marginRight: '0.5rem'}} /> contact@la-banquise.fr</a></li>
<li><a href="https://github.com/la-banquise" target="_blank" rel="noopener noreferrer"><FaGithub style={{marginRight: '0.5rem'}} /> GitHub</a></li>
</ul>
</div>
</div>
<div className="footer-bottom">
<p>&copy; {new Date().getFullYear()} La Banquise. Tous droits réservés.</p>
</div>
</footer>
</div>
);
}
export default App;