Initial commit

This commit is contained in:
KIRITO_KMA 2026-02-06 18:36:03 +01:00
commit c38fde40b0
33 changed files with 7319 additions and 0 deletions

331
assets/css/404.css Normal file
View file

@ -0,0 +1,331 @@
:root {
--bg-deep: #0a0a0f;
--bg-primary: #12121a;
--bg-secondary: #1a1a25;
--accent-primary: #8b3a62;
--accent-secondary: #5e2a8b;
--accent-light: #d4a5c9;
--text-primary: #ffffff;
--text-secondary: #b8b8c8;
--text-muted: #6b6b7b;
--glass-bg: rgba(255, 255, 255, 0.03);
--glass-border: rgba(255, 255, 255, 0.08);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Montserrat', sans-serif;
background: var(--bg-deep);
color: var(--text-primary);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
}
.bg-grid {
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(139, 58, 98, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(139, 58, 98, 0.03) 1px, transparent 1px);
background-size: 50px 50px;
pointer-events: none;
}
.bg-glow {
position: fixed;
width: 600px;
height: 600px;
border-radius: 50%;
filter: blur(150px);
opacity: 0.15;
pointer-events: none;
}
.bg-glow-1 {
top: -200px;
left: -200px;
background: var(--accent-primary);
animation: float 8s ease-in-out infinite;
}
.bg-glow-2 {
bottom: -200px;
right: -200px;
background: var(--accent-secondary);
animation: float 8s ease-in-out infinite reverse;
}
@keyframes float {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(30px, 30px); }
}
.particles {
position: fixed;
inset: 0;
pointer-events: none;
overflow: hidden;
}
.particle {
position: absolute;
width: 4px;
height: 4px;
background: var(--accent-light);
border-radius: 50%;
opacity: 0.3;
animation: rise 10s linear infinite;
}
@keyframes rise {
0% { transform: translateY(100vh) scale(0); opacity: 0; }
10% { opacity: 0.3; }
90% { opacity: 0.3; }
100% { transform: translateY(-100vh) scale(1); opacity: 0; }
}
.container {
text-align: center;
z-index: 10;
padding: 2rem;
max-width: 600px;
}
.error-code {
font-size: clamp(8rem, 25vw, 14rem);
font-weight: 700;
line-height: 1;
position: relative;
margin-bottom: 1rem;
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary), var(--accent-light));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: glitch 3s infinite;
cursor: pointer;
user-select: none;
transition: transform 0.3s ease, filter 0.3s ease;
}
.error-code:hover {
transform: scale(1.05);
filter: brightness(1.2);
}
.error-code:active {
transform: scale(0.98);
}
.error-code::before,
.error-code::after {
content: '404';
position: absolute;
top: 0;
left: 0;
right: 0;
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
opacity: 0.8;
}
.error-code::before {
animation: glitch-1 0.3s infinite linear alternate-reverse;
clip-path: polygon(0 0, 100% 0, 100% 35%, 0 35%);
}
.error-code::after {
animation: glitch-2 0.3s infinite linear alternate-reverse;
clip-path: polygon(0 65%, 100% 65%, 100% 100%, 0 100%);
}
@keyframes glitch {
0%, 90%, 100% { transform: translate(0); }
92% { transform: translate(-2px, 2px); }
94% { transform: translate(2px, -2px); }
96% { transform: translate(-2px, -2px); }
98% { transform: translate(2px, 2px); }
}
@keyframes glitch-1 {
0% { transform: translate(0); }
100% { transform: translate(-3px, 1px); }
}
@keyframes glitch-2 {
0% { transform: translate(0); }
100% { transform: translate(3px, -1px); }
}
.error-icon {
width: 80px;
height: 80px;
margin: 0 auto 2rem;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
animation: pulse-icon 2s ease-in-out infinite;
}
.error-icon svg {
width: 40px;
height: 40px;
color: var(--accent-light);
}
@keyframes pulse-icon {
0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(139, 58, 98, 0.4); }
50% { transform: scale(1.05); box-shadow: 0 0 0 15px rgba(139, 58, 98, 0); }
}
.error-title {
font-size: 1.75rem;
font-weight: 600;
margin-bottom: 1rem;
color: var(--text-primary);
}
.error-message {
font-size: 1rem;
color: var(--text-muted);
line-height: 1.7;
margin-bottom: 2.5rem;
}
.error-message span {
color: var(--accent-light);
}
.buttons {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.875rem 1.75rem;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 600;
text-decoration: none;
transition: all 0.3s ease;
cursor: pointer;
border: none;
}
.btn svg { width: 18px; height: 18px; }
.btn-primary {
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
color: white;
box-shadow: 0 4px 20px rgba(139, 58, 98, 0.3);
}
.btn-primary:hover {
transform: translateY(-3px);
box-shadow: 0 8px 30px rgba(139, 58, 98, 0.4);
}
.btn-secondary {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
color: var(--text-secondary);
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.08);
border-color: var(--accent-primary);
color: var(--text-primary);
}
.portal {
position: absolute;
width: 300px;
height: 300px;
pointer-events: none;
opacity: 0.1;
}
.portal-left { left: -100px; top: 50%; transform: translateY(-50%); }
.portal-right { right: -100px; top: 50%; transform: translateY(-50%); }
.portal-ring {
position: absolute;
inset: 0;
border: 2px solid var(--accent-primary);
border-radius: 50%;
animation: rotate 10s linear infinite;
}
.portal-ring:nth-child(2) { inset: 20px; border-color: var(--accent-secondary); animation-duration: 8s; animation-direction: reverse; }
.portal-ring:nth-child(3) { inset: 40px; border-color: var(--accent-light); animation-duration: 6s; }
@keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.footer {
position: fixed;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--text-muted);
font-size: 0.85rem;
z-index: 10;
}
.footer a { color: var(--accent-light); text-decoration: none; transition: color 0.3s ease; }
.footer a:hover { color: var(--text-primary); }
.logo-mini {
font-weight: 700;
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Responsive 404 */
@media (max-width: 768px) {
.error-code { font-size: clamp(5rem, 20vw, 8rem); }
.error-icon { width: 60px; height: 60px; margin-bottom: 1.5rem; }
.error-icon svg { width: 30px; height: 30px; }
.error-title { font-size: 1.4rem; }
.error-message { font-size: 0.9rem; padding: 0 1rem; }
.buttons { flex-direction: column; width: 100%; padding: 0 1rem; }
.btn { width: 100%; justify-content: center; padding: 1rem 1.5rem; }
.portal { display: none; }
.footer { bottom: 1rem; font-size: 0.75rem; }
}
@media (max-width: 480px) {
.container { padding: 1rem; }
.error-code { font-size: clamp(4rem, 18vw, 6rem); margin-bottom: 0.5rem; }
.error-icon { width: 50px; height: 50px; margin-bottom: 1rem; }
.error-icon svg { width: 24px; height: 24px; }
.error-title { font-size: 1.2rem; }
.error-message { font-size: 0.85rem; margin-bottom: 2rem; }
.btn { font-size: 0.9rem; padding: 0.875rem 1.25rem; }
}
@media (max-height: 600px) {
.error-icon { display: none; }
.error-code { margin-bottom: 0.5rem; }
.error-title { font-size: 1.2rem; margin-bottom: 0.5rem; }
.error-message { margin-bottom: 1.5rem; }
}

33
assets/css/legal.css Normal file
View file

@ -0,0 +1,33 @@
/* assets/css/legal.css - minimal rules for legal pages */
/* Layout background patterns */
.geo-bg { position:absolute; inset:0; background:radial-gradient(#0f1724 0%, transparent 40%); z-index:0; }
.grid-pattern { position:absolute; inset:0; background-image: repeating-linear-gradient(45deg, rgba(255,255,255,0.02) 0 1px, transparent 1px 20px); z-index:0; }
/* Hero */
.page-hero { display:flex; align-items:center; justify-content:center; padding:3rem 1rem; background:var(--hero-bg); color:var(--text-on-hero); }
.page-hero .hero-content { max-width:1100px; text-align:center; }
/* Legal content (text-first layout) */
.legal-content { padding:2.25rem 1rem; background:transparent; color:var(--text); }
.legal-container { max-width:900px; margin:0 auto; display:block; }
.legal-section { margin-bottom:1.25rem; }
.legal-section h2 { font-size:1.1rem; margin:0 0 .5rem 0; font-weight:700; display:block; }
.legal-card { background:transparent; padding:0; border-radius:0; box-shadow:none; border:none; }
.legal-card p, .legal-card ul { margin:0 0 .75rem 0; line-height:1.6; color:var(--text); }
.page-hero { padding:1.5rem 1rem; background:transparent; color:var(--text); text-align:left; }
.page-hero .hero-content h1 { font-size:1.6rem; margin-bottom:.25rem; }
.page-hero .hero-content p { margin:0 0 .5rem 0; }
/* Footer */
footer .footer-content { display:flex; gap:1rem; flex-wrap:wrap; align-items:center; }
.footer-bottom { text-align:center; padding:1rem 0; font-size:.9rem; }
/* Nav / responsive */
#navbar.scrolled { backdrop-filter: blur(4px); box-shadow:0 6px 18px rgba(2,6,23,.12); }
.nav-links { display:flex; gap:1rem; list-style:none; }
.nav-links a.active, .nav-links a[aria-current="true"] { font-weight:700; color:var(--accent); }
.nav-links.active { display:block; }
.mobile-toggle { display:none; /* show via media query */ }
@media (max-width:880px) { .nav-links { display:none; } .mobile-toggle { display:block; } }

3552
assets/css/main.css Normal file

File diff suppressed because it is too large Load diff

290
assets/data/projects.json Normal file
View file

@ -0,0 +1,290 @@
[
{
"id": 1,
"category": "bot",
"title": "Medieval RP Bot",
"desc": "Un système RP médiéval complet avec économie, métiers, combat stratégique et gestion de royaumes. Développé pour une communauté de 5000+ joueurs actifs.",
"gradient": "linear-gradient(135deg, var(--accent-primary), var(--accent-secondary))",
"year": "2024",
"duration": "3 mois",
"features": ["Système d'économie", "Combat tour par tour", "Gestion de royaumes", "Quêtes automatisées", "Inventaire avancé", "Système de métiers"],
"tech": ["Python", "Discord.py", "PostgreSQL", "Redis", "FastAPI"],
"stats": [{ "value": "5000+", "label": "Joueurs" }, { "value": "150+", "label": "Commandes" }, { "value": "99.9%", "label": "Uptime" }]
},
{
"id": 2,
"category": "map",
"title": "Cyberpunk City",
"desc": "Ville futuriste immersive avec 5 quartiers distincts, plus de 100 bâtiments accessibles et un système jour/nuit dynamique. Optimisée pour 50 joueurs.",
"gradient": "linear-gradient(135deg, #5E2040, #8b3a62)",
"year": "2024",
"duration": "2 mois",
"features": ["5 quartiers uniques", "100+ bâtiments", "Système jour/nuit", "Zones scriptées", "Éclairage néon", "Optimisé 50 joueurs"],
"tech": ["Fortnite Creative", "UEFN", "Verse", "Lighting"],
"stats": [{ "value": "50K+", "label": "Parties" }, { "value": "100+", "label": "Bâtiments" }, { "value": "50", "label": "Joueurs max" }]
},
{
"id": 3,
"category": "bot",
"title": "Space Station RP",
"desc": "Bot Sci-Fi avec économie spatiale, gestion de vaisseaux, exploration de planètes et système de factions interstellaires.",
"gradient": "linear-gradient(135deg, #3a1f5e, #8b3a62)",
"year": "2024",
"duration": "4 mois",
"features": ["Économie spatiale", "Gestion de vaisseaux", "Exploration", "Factions", "Combat spatial", "Crafting"],
"tech": ["Python", "Discord.py", "MongoDB", "Docker"],
"stats": [{ "value": "3000+", "label": "Joueurs" }, { "value": "200+", "label": "Commandes" }, { "value": "50+", "label": "Planètes" }]
},
{
"id": 4,
"category": "graphisme",
"title": "Branding Nova RP",
"desc": "Identité visuelle complète incluant logo, bannières, emojis personnalisés et templates pour un serveur RP majeur.",
"gradient": "linear-gradient(135deg, #2d1b5e, #5E2040)",
"year": "2024",
"duration": "3 semaines",
"features": ["Logo principal", "Déclinaisons logo", "Bannières Discord", "Emojis custom", "Templates", "Guide de style"],
"tech": ["Photoshop", "Illustrator", "After Effects", "Figma"],
"stats": [{ "value": "50+", "label": "Éléments" }, { "value": "8", "label": "Bannières" }, { "value": "30+", "label": "Emojis" }]
},
{
"id": 5,
"category": "map",
"title": "Tokyo Night",
"desc": "Reconstitution d'un quartier de Tokyo nocturne avec ruelles, néons, boutiques et ambiance authentique pour du RP urbain japonais.",
"gradient": "linear-gradient(135deg, #1a3a5e, #3a5e8b)",
"year": "2024",
"duration": "6 semaines",
"features": ["Ruelles détaillées", "Néons japonais", "Boutiques", "Metro", "Appartements", "Rooftops"],
"tech": ["Fortnite Creative", "UEFN", "Custom Props"],
"stats": [{ "value": "30K+", "label": "Parties" }, { "value": "80+", "label": "Boutiques" }, { "value": "40", "label": "Joueurs max" }]
},
{
"id": 6,
"category": "bot",
"title": "Western Frontier",
"desc": "Système RP Far West complet avec duels, shérif, hors-la-loi, prospection d'or et gestion de ranch.",
"gradient": "linear-gradient(135deg, #5e3a1a, #8b5e3a)",
"year": "2023",
"duration": "3 mois",
"features": ["Système de duels", "Rôles Sheriff/Outlaw", "Prospection d'or", "Gestion de ranch", "Économie western", "Wanted system"],
"tech": ["Python", "Discord.py", "SQLite", "PIL"],
"stats": [{ "value": "2500+", "label": "Joueurs" }, { "value": "100+", "label": "Commandes" }, { "value": "15+", "label": "Métiers" }]
},
{
"id": 7,
"category": "graphisme",
"title": "EcoVerse Assets",
"desc": "Pack complet d'icônes et emojis sur thème écologique/nature pour serveur communautaire gaming.",
"gradient": "linear-gradient(135deg, #1a5e3a, #3a8b5e)",
"year": "2024",
"duration": "2 semaines",
"features": ["Pack icônes", "Emojis animés", "Badges ranks", "Séparateurs", "Backgrounds", "Overlays"],
"tech": ["Illustrator", "After Effects", "Photoshop"],
"stats": [{ "value": "100+", "label": "Icônes" }, { "value": "50+", "label": "Emojis" }, { "value": "20", "label": "Badges" }]
},
{
"id": 8,
"category": "map",
"title": "Apocalypse Zone",
"desc": "Monde post-apocalyptique avec zones de survie, bunkers, véhicules abandonnés et ambiance désolée.",
"gradient": "linear-gradient(135deg, #5e1a3a, #8b3a5e)",
"year": "2023",
"duration": "2 mois",
"features": ["Zones dévastées", "Bunkers secrets", "Véhicules props", "Camps survivants", "Ressources", "Zone radioactive"],
"tech": ["Fortnite Creative", "UEFN", "Environmental Design"],
"stats": [{ "value": "25K+", "label": "Parties" }, { "value": "10", "label": "Zones" }, { "value": "30", "label": "Joueurs max" }]
},
{
"id": 9,
"category": "bot",
"title": "Academy RP",
"desc": "Système scolaire interactif avec cours, examens, clubs, dortoirs et système de réputation étudiant.",
"gradient": "linear-gradient(135deg, #3a1a5e, #5e3a8b)",
"year": "2024",
"duration": "2 mois",
"features": ["Système de cours", "Examens", "Clubs étudiants", "Dortoirs", "Réputation", "Events scolaires"],
"tech": ["Python", "Discord.py", "PostgreSQL"],
"stats": [{ "value": "4000+", "label": "Étudiants" }, { "value": "50+", "label": "Cours" }, { "value": "20+", "label": "Clubs" }]
},
{
"id": 10,
"category": "graphisme",
"title": "Golden Crown",
"desc": "Bannières et visuels premium style royal/luxe pour serveur RP médiéval haut de gamme.",
"gradient": "linear-gradient(135deg, #5e5e1a, #8b8b3a)",
"year": "2024",
"duration": "1 mois",
"features": ["Bannières premium", "Logo royal", "Ornements", "Cadres dorés", "Typography", "Animations"],
"tech": ["Photoshop", "Illustrator", "After Effects"],
"stats": [{ "value": "30+", "label": "Visuels" }, { "value": "5", "label": "Animations" }, { "value": "100%", "label": "Satisfaction" }]
},
{
"id": 11,
"category": "map",
"title": "Mystic Village",
"desc": "Village fantastique médiéval avec magie, créatures et architecture féerique pour RP fantasy.",
"gradient": "linear-gradient(135deg, #1a1a5e, #3a3a8b)",
"year": "2023",
"duration": "5 semaines",
"features": ["Village médiéval", "Tour du mage", "Forêt enchantée", "Taverne", "Marché", "Arène"],
"tech": ["Fortnite Creative", "Custom Lighting"],
"stats": [{ "value": "20K+", "label": "Parties" }, { "value": "50+", "label": "Bâtiments" }, { "value": "35", "label": "Joueurs max" }]
},
{
"id": 12,
"category": "bot",
"title": "Crime City",
"desc": "Bot GTA-like avec gangs, territoire, trafics, police et système judiciaire complet.",
"gradient": "linear-gradient(135deg, #5e1a1a, #8b3a3a)",
"year": "2024",
"duration": "4 mois",
"features": ["Système de gangs", "Territoires", "Trafics", "Police/Justice", "Prison", "Braquages"],
"tech": ["Python", "Discord.py", "PostgreSQL", "Redis"],
"stats": [{ "value": "6000+", "label": "Joueurs" }, { "value": "250+", "label": "Commandes" }, { "value": "10+", "label": "Gangs" }]
},
{
"id": 13,
"category": "bot",
"title": "Pirate Bay RP",
"desc": "Aventure pirate complète avec navigation, batailles navales, trésors et commerce maritime.",
"gradient": "linear-gradient(135deg, #1a4a5e, #3a6a8b)",
"year": "2023",
"duration": "3 mois",
"features": ["Navigation", "Batailles navales", "Chasse au trésor", "Commerce", "Équipages", "Îles secrètes"],
"tech": ["Python", "Discord.py", "MongoDB"],
"stats": [{ "value": "3500+", "label": "Pirates" }, { "value": "100+", "label": "Îles" }, { "value": "50+", "label": "Navires" }]
},
{
"id": 14,
"category": "map",
"title": "Neon District",
"desc": "Quartier cyberpunk détaillé avec clubs, appartements high-tech et ambiance nocturne.",
"gradient": "linear-gradient(135deg, #4a1a5e, #6a3a8b)",
"year": "2024",
"duration": "4 semaines",
"features": ["Nightclubs", "Appartements", "Ruelles néon", "Boutiques tech", "Garages", "Rooftop bars"],
"tech": ["Fortnite Creative", "UEFN", "Neon Effects"],
"stats": [{ "value": "15K+", "label": "Parties" }, { "value": "60+", "label": "Intérieurs" }, { "value": "25", "label": "Joueurs max" }]
},
{
"id": 15,
"category": "graphisme",
"title": "Cyber UI Pack",
"desc": "Interface futuriste complète avec HUD, menus et éléments interactifs style sci-fi.",
"gradient": "linear-gradient(135deg, #1a5e5e, #3a8b8b)",
"year": "2024",
"duration": "3 semaines",
"features": ["HUD elements", "Menus", "Boutons", "Barres de vie", "Minimap style", "Notifications"],
"tech": ["Figma", "Illustrator", "After Effects"],
"stats": [{ "value": "80+", "label": "Éléments" }, { "value": "10", "label": "Animations" }, { "value": "3", "label": "Thèmes" }]
},
{
"id": 16,
"category": "bot",
"title": "Dragon Realm",
"desc": "Univers fantasy avec dragons apprivoisables, magie élémentaire et quêtes épiques.",
"gradient": "linear-gradient(135deg, #5e2a1a, #8b4a3a)",
"year": "2023",
"duration": "5 mois",
"features": ["Dragons", "Magie élémentaire", "Quêtes épiques", "Guildes", "Donjons", "Boss fights"],
"tech": ["Python", "Discord.py", "PostgreSQL", "PIL"],
"stats": [{ "value": "4500+", "label": "Joueurs" }, { "value": "30+", "label": "Dragons" }, { "value": "100+", "label": "Quêtes" }]
},
{
"id": 17,
"category": "map",
"title": "Space Colony",
"desc": "Station spatiale habitable avec modules, serres, hangars et vue sur l'espace.",
"gradient": "linear-gradient(135deg, #1a2a5e, #3a4a8b)",
"year": "2024",
"duration": "6 semaines",
"features": ["Modules habitables", "Serres spatiales", "Hangars", "Passerelles", "Vue espace", "Gravité zéro"],
"tech": ["Fortnite Creative", "UEFN", "Sci-fi Props"],
"stats": [{ "value": "12K+", "label": "Parties" }, { "value": "20", "label": "Modules" }, { "value": "20", "label": "Joueurs max" }]
},
{
"id": 18,
"category": "graphisme",
"title": "Retro Wave Pack",
"desc": "Style années 80 revisité avec néons, grilles et esthétique synthwave.",
"gradient": "linear-gradient(135deg, #5e1a4a, #8b3a6a)",
"year": "2023",
"duration": "2 semaines",
"features": ["Bannières retro", "Grilles 80s", "Néons rose/bleu", "Typography", "Sun effects", "VHS overlays"],
"tech": ["Photoshop", "After Effects", "Illustrator"],
"stats": [{ "value": "40+", "label": "Éléments" }, { "value": "15", "label": "Animations" }, { "value": "5", "label": "Templates" }]
},
{
"id": 19,
"category": "bot",
"title": "Zombie Survival",
"desc": "Système survie zombie avec crafting, bases, hordes et coopération entre survivants.",
"gradient": "linear-gradient(135deg, #2a5e1a, #4a8b3a)",
"year": "2024",
"duration": "3 mois",
"features": ["Système de survie", "Crafting", "Construction bases", "Hordes zombies", "Coop", "Ressources"],
"tech": ["Python", "Discord.py", "MongoDB", "Redis"],
"stats": [{ "value": "5500+", "label": "Survivants" }, { "value": "50+", "label": "Items craft" }, { "value": "1000+", "label": "Bases" }]
},
{
"id": 20,
"category": "map",
"title": "Desert Oasis",
"desc": "Monde désertique immersif avec oasis, ruines anciennes et caravanes nomades.",
"gradient": "linear-gradient(135deg, #5e4a1a, #8b6a3a)",
"year": "2023",
"duration": "5 semaines",
"features": ["Désert vaste", "Oasis centrale", "Ruines anciennes", "Camp nomade", "Tempêtes sable", "Grottes"],
"tech": ["Fortnite Creative", "Environmental FX"],
"stats": [{ "value": "18K+", "label": "Parties" }, { "value": "8", "label": "Zones" }, { "value": "30", "label": "Joueurs max" }]
},
{
"id": 21,
"category": "bot",
"title": "Mafia Empire",
"desc": "Organisation criminelle RP avec familles, territoires, business illégaux et omerta.",
"gradient": "linear-gradient(135deg, #3a3a3a, #5a5a5a)",
"year": "2024",
"duration": "4 mois",
"features": ["Familles mafieuses", "Territoires", "Business", "Omerta", "Hiérarchie", "Contrats"],
"tech": ["Python", "Discord.py", "PostgreSQL"],
"stats": [{ "value": "3800+", "label": "Membres" }, { "value": "8", "label": "Familles" }, { "value": "150+", "label": "Commandes" }]
},
{
"id": 22,
"category": "graphisme",
"title": "Fantasy Icons",
"desc": "200+ icônes médiévales pour items, skills, classes et interface de jeu RP.",
"gradient": "linear-gradient(135deg, #4a3a1a, #6a5a3a)",
"year": "2024",
"duration": "1 mois",
"features": ["Icônes items", "Icônes skills", "Icônes classes", "Cadres", "Rarités", "Animations"],
"tech": ["Illustrator", "Photoshop"],
"stats": [{ "value": "200+", "label": "Icônes" }, { "value": "6", "label": "Rarités" }, { "value": "12", "label": "Classes" }]
},
{
"id": 23,
"category": "map",
"title": "Underwater City",
"desc": "Cité sous-marine style Atlantis avec dômes, tunnels et créatures marines.",
"gradient": "linear-gradient(135deg, #1a3a4a, #3a5a6a)",
"year": "2024",
"duration": "7 semaines",
"features": ["Dômes habitables", "Tunnels verre", "Jardins marins", "Palais central", "Sous-marins", "Créatures"],
"tech": ["Fortnite Creative", "UEFN", "Underwater FX"],
"stats": [{ "value": "22K+", "label": "Parties" }, { "value": "15", "label": "Dômes" }, { "value": "25", "label": "Joueurs max" }]
},
{
"id": 24,
"category": "bot",
"title": "Superhero League",
"desc": "Univers super-héros avec pouvoirs, missions, vilains et quartier général.",
"gradient": "linear-gradient(135deg, #1a1a4a, #3a3a6a)",
"year": "2023",
"duration": "4 mois",
"features": ["Pouvoirs custom", "Missions", "Vilains IA", "QG héros", "Réputation", "Team-ups"],
"tech": ["Python", "Discord.py", "PostgreSQL", "PIL"],
"stats": [{ "value": "4200+", "label": "Héros" }, { "value": "80+", "label": "Pouvoirs" }, { "value": "200+", "label": "Missions" }]
}
]

86
assets/data/team.json Normal file
View file

@ -0,0 +1,86 @@
[
{
"name": "Alexandre",
"role": "Développeur Principal",
"avatar": "A",
"bio": "Passionné de développement depuis l'adolescence, Alexandre a découvert le monde du RP Discord en 2019. Depuis, il a développé plus de 50 bots pour des communautés allant de quelques centaines à plusieurs milliers de membres. Son expertise couvre les systèmes d'économie, les mécaniques de jeu complexes et l'optimisation des performances.",
"stats": [
{ "value": "5", "label": "Ans d'exp." },
{ "value": "50+", "label": "Projets" },
{ "value": "★★★", "label": "Expert" }
],
"skills": ["Python", "Discord.py", "JavaScript", "Node.js", "MongoDB", "API REST"]
},
{
"name": "Sophie",
"role": "Designer Fortnite",
"avatar": "S",
"bio": "Sophie combine sa passion pour le level design et le storytelling pour créer des environnements Fortnite uniques. Formée en architecture, elle apporte une attention particulière aux détails et à l'ambiance de chaque map. Ses créations ont été jouées par plus de 100 000 joueurs.",
"stats": [
{ "value": "3", "label": "Ans d'exp." },
{ "value": "25+", "label": "Maps" },
{ "value": "100K", "label": "Joueurs" }
],
"skills": ["Fortnite Creative", "UEFN", "Level Design", "3D", "Lighting", "Scripting"]
},
{
"name": "Thomas",
"role": "Community Manager",
"avatar": "T",
"bio": "Thomas est le lien entre OmégaKube et ses communautés. Ancien admin de plusieurs serveurs RP majeurs, il comprend les besoins des joueurs et des staff. Il s'assure que chaque projet répond aux attentes et accompagne les clients dans le développement de leur communauté.",
"stats": [
{ "value": "4", "label": "Ans d'exp." },
{ "value": "20K+", "label": "Membres" },
{ "value": "24/7", "label": "Dispo" }
],
"skills": ["Community Management", "Discord", "Modération", "Events", "Support"]
},
{
"name": "Marie",
"role": "Graphiste",
"avatar": "M",
"bio": "Marie donne vie aux identités visuelles des projets RP. Diplômée en design graphique, elle maîtrise aussi bien la création de logos que le motion design. Son style unique mélange esthétique gaming et professionnalisme pour des visuels qui marquent les esprits.",
"stats": [
{ "value": "6", "label": "Ans d'exp." },
{ "value": "200+", "label": "Créations" },
{ "value": "★★★", "label": "Expert" }
],
"skills": ["Photoshop", "Illustrator", "After Effects", "Figma", "Branding", "UI Design"]
},
{
"name": "Lucas",
"role": "Développeur Backend",
"avatar": "L",
"bio": "Lucas est l'architecte invisible derrière les systèmes les plus robustes d'OmégaKube. Spécialisé dans les bases de données et les API, il conçoit des systèmes d'économie capables de gérer des millions de transactions sans ralentissement.",
"stats": [
{ "value": "4", "label": "Ans d'exp." },
{ "value": "30+", "label": "Systèmes" },
{ "value": "99.9%", "label": "Uptime" }
],
"skills": ["Python", "PostgreSQL", "Redis", "FastAPI", "Docker", "AWS"]
},
{
"name": "Emma",
"role": "Scénariste RP",
"avatar": "E",
"bio": "Emma crée les univers et les histoires qui donnent vie aux sessions RP. Autrice de formation, elle conçoit des scénarios immersifs, des personnages mémorables et des quêtes captivantes. Elle aide aussi les clients à développer le lore de leur serveur.",
"stats": [
{ "value": "3", "label": "Ans d'exp." },
{ "value": "40+", "label": "Scénarios" },
{ "value": "∞", "label": "Créativité" }
],
"skills": ["Écriture", "Worldbuilding", "Game Design", "Narration", "Dialogue"]
},
{
"name": "Julien",
"role": "Testeur QA",
"avatar": "J",
"bio": "Julien est le gardien de la qualité chez OmégaKube. Aucun bug ne lui échappe. Il teste méticuleusement chaque fonctionnalité, chaque commande, chaque interaction pour garantir une expérience sans accroc aux utilisateurs finaux.",
"stats": [
{ "value": "2", "label": "Ans d'exp." },
{ "value": "500+", "label": "Bugs trouvés" },
{ "value": "0", "label": "Bug raté" }
],
"skills": ["Testing", "Bug Tracking", "Documentation", "Discord", "Feedback"]
}
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

BIN
assets/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

BIN
assets/images/old-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

171
assets/js/home.js Normal file
View file

@ -0,0 +1,171 @@
// Home page specific JS: team carousel + modal
document.addEventListener('DOMContentLoaded', () => {
// Fetch team data and render
fetch('assets/data/team.json')
.then(res => res.json())
.then(members => {
initTeamSection(members);
})
.catch(err => {
console.error('Failed to load team data', err);
});
function initTeamSection(members) {
const carousel = document.getElementById('team-carousel');
const dotsContainer = document.getElementById('carousel-dots');
const prevBtn = document.getElementById('carousel-prev');
const nextBtn = document.getElementById('carousel-next');
// Render cards
carousel.innerHTML = members.map((m, i) => `
<div class="team-card" data-member="${i}">
<div class="team-avatar">
<div class="team-avatar-inner">${m.avatar}</div>
</div>
<h4>${m.name}</h4>
<p class="team-role">${m.role}</p>
<p class="team-desc">${(m.bio || '').split('.').shift()}.</p>
</div>
`).join('');
// Carousel logic
const cardsContainer = carousel; // the track
let cards = carousel.querySelectorAll('.team-card');
let currentIndex = 0;
let cardsPerView = 3;
let autoPlayInterval;
function updateCardsPerView() {
if (window.innerWidth <= 768) cardsPerView = 1;
else if (window.innerWidth <= 1024) cardsPerView = 2;
else cardsPerView = 3;
}
function createDots() {
dotsContainer.innerHTML = '';
const totalDots = Math.max(1, Math.ceil(cards.length / cardsPerView));
for (let i = 0; i < totalDots; i++) {
const dot = document.createElement('div');
dot.classList.add('carousel-dot');
if (i === 0) dot.classList.add('active');
dot.addEventListener('click', () => goToSlide(i));
dotsContainer.appendChild(dot);
}
}
function updateCarousel() {
cards = carousel.querySelectorAll('.team-card');
if (cards.length === 0) return;
const cardStyle = window.getComputedStyle(cards[0]);
const gap = 32; // matches previous implementation
const cardWidth = cards[0].offsetWidth + gap;
carousel.style.transform = `translateX(-${currentIndex * cardWidth}px)`;
const dots = dotsContainer.querySelectorAll('.carousel-dot');
const activeDotIndex = Math.floor(currentIndex / cardsPerView);
dots.forEach((dot, i) => dot.classList.toggle('active', i === activeDotIndex));
}
function goToSlide(dotIndex) {
currentIndex = dotIndex * cardsPerView;
if (currentIndex > cards.length - cardsPerView) currentIndex = Math.max(0, cards.length - cardsPerView);
updateCarousel();
resetAutoPlay();
}
function nextSlide() {
currentIndex++;
if (currentIndex > cards.length - cardsPerView) currentIndex = 0;
updateCarousel();
}
function prevSlide() {
currentIndex--;
if (currentIndex < 0) currentIndex = Math.max(0, cards.length - cardsPerView);
updateCarousel();
}
function startAutoPlay() {
clearInterval(autoPlayInterval);
autoPlayInterval = setInterval(nextSlide, 4000);
}
function resetAutoPlay() {
clearInterval(autoPlayInterval);
startAutoPlay();
}
prevBtn && prevBtn.addEventListener('click', () => { prevSlide(); resetAutoPlay(); });
nextBtn && nextBtn.addEventListener('click', () => { nextSlide(); resetAutoPlay(); });
carousel.addEventListener('mouseenter', () => clearInterval(autoPlayInterval));
carousel.addEventListener('mouseleave', startAutoPlay);
updateCardsPerView();
createDots();
startAutoPlay();
window.addEventListener('resize', () => {
updateCardsPerView();
createDots();
currentIndex = 0;
updateCarousel();
});
// Modal logic
const modalOverlay = document.getElementById('team-modal-overlay');
const modalClose = document.getElementById('team-modal-close');
const modalAvatar = document.getElementById('modal-avatar');
const modalName = document.getElementById('modal-name');
const modalRole = document.getElementById('modal-role');
const modalBio = document.getElementById('modal-bio');
const modalStats = document.getElementById('modal-stats');
const modalSkills = document.getElementById('modal-skills');
const modalSocial = document.getElementById('modal-social');
function openMemberModal(index) {
const member = members[index];
if (!member) return;
modalAvatar.textContent = member.avatar || member.name.charAt(0);
modalName.textContent = member.name;
modalRole.textContent = member.role;
modalBio.textContent = member.bio;
modalStats.innerHTML = (member.stats || []).map(stat => `
<div class="team-modal-stat">
<div class="team-modal-stat-value">${stat.value}</div>
<div class="team-modal-stat-label">${stat.label}</div>
</div>
`).join('');
modalSkills.innerHTML = (member.skills || []).map(s => `<span class="team-modal-skill">${s}</span>`).join('');
modalSocial.innerHTML = `
<a href="#" aria-label="Discord"><i data-feather="message-circle"></i></a>
<a href="#" aria-label="Twitter"><i data-feather="twitter"></i></a>
`;
if (window.feather && feather.replace) feather.replace();
modalOverlay.classList.add('active');
document.body.style.overflow = 'hidden';
}
function closeMemberModal() {
modalOverlay.classList.remove('active');
document.body.style.overflow = '';
}
// Attach click listeners to generated cards
carousel.querySelectorAll('.team-card').forEach(card => {
card.addEventListener('click', () => {
const idx = parseInt(card.dataset.member, 10);
openMemberModal(idx);
});
});
modalClose && modalClose.addEventListener('click', closeMemberModal);
modalOverlay && modalOverlay.addEventListener('click', (e) => { if (e.target === modalOverlay) closeMemberModal(); });
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && modalOverlay.classList.contains('active')) closeMemberModal(); });
}
});

393
assets/js/main.js Normal file
View file

@ -0,0 +1,393 @@
// OmégaKube - shared scripts
// Feather icons loader (centralized)
// loads https://unpkg.com/feather-icons when needed and exposes window.ensureFeather()
const FEATHER_URL = 'https://unpkg.com/feather-icons';
function ensureFeather() {
if (window.__featherLoading) return window.__featherLoading;
if (window.feather && window.feather.replace) return Promise.resolve(window.feather);
window.__featherLoading = new Promise((resolve, reject) => {
const s = document.createElement('script');
s.src = FEATHER_URL;
s.async = true;
s.onload = () => resolve(window.feather);
s.onerror = () => reject(new Error('Failed to load feather-icons'));
document.head.appendChild(s);
});
return window.__featherLoading;
}
window.ensureFeather = ensureFeather;
document.addEventListener('DOMContentLoaded', async () => {
// ensure feather icons are available and replace icons in document
try { await ensureFeather(); if (window.feather && feather.replace) try { feather.replace(); } catch(e) {} } catch (e) {}
// Load HTML includes (components header/footer) and fix relative paths when necessary
async function loadIncludes() {
const includeSelector = '[data-include]';
const nodes = Array.from(document.querySelectorAll(includeSelector));
if (!nodes.length) return;
await Promise.all(nodes.map(async el => {
const url = el.getAttribute('data-include');
try {
const res = await fetch(url);
if (!res.ok) throw new Error(res.statusText);
const text = await res.text();
const template = document.createElement('template');
template.innerHTML = text;
const fragment = template.content;
// adjust relative paths for assets and page links when included from /pages/
fragment.querySelectorAll('[href], [src]').forEach(node => {
['href','src'].forEach(attr => {
if (!node.hasAttribute(attr)) return;
const original = node.getAttribute(attr);
// leave absolute URLs and anchors untouched
if (/^(https?:|\/|#)/.test(original)) return;
// compute useful bases
const includeUrl = new URL(url, document.baseURI);
const includeDir = includeUrl.pathname.substring(0, includeUrl.pathname.lastIndexOf('/') + 1);
const docUrl = new URL(document.location.href);
const docDir = docUrl.pathname.substring(0, docUrl.pathname.lastIndexOf('/') + 1);
// find common prefix between include location and current document to infer site root
const includeParts = includeDir.split('/');
const docParts = docDir.split('/');
let i = 0;
while (i < includeParts.length && i < docParts.length && includeParts[i] === docParts[i]) i++;
const baseRoot = includeParts.slice(0, i).join('/') || '/';
let newVal;
if (original.startsWith('./') || original.startsWith('../')) {
// explicit relative paths should resolve relative to the include file
const resolved = new URL(original, includeUrl);
newVal = resolved.pathname + resolved.search + resolved.hash;
} else {
// treat plain paths (e.g. "assets/..." or "pages/...") as site-root relative
newVal = (baseRoot.endsWith('/') ? baseRoot : baseRoot + '/') + original;
}
node.setAttribute(attr, newVal);
});
});
el.replaceWith(fragment);
} catch (err) {
console.error('Failed to load include', url, err);
}
}));
if (typeof ensureFeather === 'function') {
ensureFeather().then(() => { try { if (window.feather && feather.replace) feather.replace(); } catch (e) {} }).catch(()=>{});
} else if (window.feather && feather.replace) {
try { feather.replace(); } catch (e) {}
}
}
// wait for includes to be loaded so subsequent initialization can bind to elements inside them
await loadIncludes();
const html = document.documentElement;
function getPreferredTheme() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme) return savedTheme;
return window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
}
// Apply preferred theme
try {
html.setAttribute('data-theme', getPreferredTheme());
} catch (e) {}
// Theme toggle (robust)
const themeToggle = document.getElementById('theme-toggle');
if (themeToggle) {
// sync aria state
themeToggle.setAttribute('aria-pressed', html.getAttribute('data-theme') === 'dark' ? 'false' : 'true');
themeToggle.addEventListener('click', () => {
const currentTheme = html.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
html.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
themeToggle.setAttribute('aria-pressed', newTheme === 'dark' ? 'false' : 'true');
});
}
// Navbar scroll effect
const navbar = document.getElementById('navbar');
function updateNavbarOnScroll() {
if (!navbar) return;
navbar.classList.toggle('scrolled', window.scrollY > 50);
}
window.addEventListener('scroll', updateNavbarOnScroll);
window.addEventListener('load', updateNavbarOnScroll);
// Mobile menu
const mobileToggle = document.getElementById('mobile-toggle');
const navLinks = document.getElementById('nav-links');
// mobile menu initialization (debug logs removed)
// create a toggle function that is safe even if elements are added later
const toggleMenu = () => {
const mt = document.getElementById('mobile-toggle');
const nl = document.getElementById('nav-links');
// console.log('[DEBUG] toggleMenu called', { mt: !!mt, nl: !!nl, mtClass: mt ? mt.className : null, nlClass: nl ? nl.className : null });
if (!mt || !nl) {
// elements missing: abort toggle
// console.log('[DEBUG] toggleMenu aborted: missing elements', { mt, nl });
return;
}
mt.classList.toggle('active');
nl.classList.toggle('active');
// console.log('[DEBUG] toggleMenu toggled', { mtClass: mt.className, nlClass: nl.className });
const expanded = nl.classList.contains('active') ? 'true' : 'false';
mt.setAttribute('aria-expanded', expanded);
};
// |||||||||||||| ça fait buguer le menu mobile ||||||||||||||
// direct binding if elements exist
// if (mobileToggle && navLinks) {
// if (!mobileToggle.hasAttribute('tabindex')) mobileToggle.setAttribute('tabindex', '0');
// mobileToggle.setAttribute('aria-expanded', navLinks.classList.contains('active') ? 'true' : 'false');
// mobileToggle.addEventListener('click', (e) => {
// console.log('[DEBUG] mobileToggle click handler fired', e.target, e);
// e.preventDefault();
// toggleMenu();
// });
// mobileToggle.addEventListener('keydown', (e) => {
// if (e.key === 'Enter' || e.key === ' ') {
// console.log('[DEBUG] mobileToggle keydown:', e.key);
// e.preventDefault();
// toggleMenu();
// }
// });
// navLinks.querySelectorAll('a').forEach(link => {
// link.addEventListener('click', () => {
// console.log('[DEBUG] nav link clicked in mobile menu', link.getAttribute('href'));
// // collapse on link click
// const mt = document.getElementById('mobile-toggle');
// const nl = document.getElementById('nav-links');
// if (mt && nl) {
// mt.classList.remove('active');
// nl.classList.remove('active');
// mt.setAttribute('aria-expanded', 'false');
// }
// });
// });
// }
// Global delegation: catch clicks anywhere on page for '#mobile-toggle' (works even if element is replaced)
document.addEventListener('click', (e) => {
const mt = e.target.closest ? e.target.closest('#mobile-toggle') : null;
if (mt) {
// console.log('[DEBUG] delegated click caught for #mobile-toggle', e.target);
e.preventDefault();
toggleMenu();
}
});
// expose helper for console debugging
window.toggleMobileMenu = toggleMenu;
// Smooth scroll for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
const href = this.getAttribute('href');
if (!href || href === '#') return;
const target = document.querySelector(href);
if (target) {
e.preventDefault();
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
});
// Highlight active nav link based on current URL
function normalizePath(path) {
if (!path) return '/';
// treat both '/' and '/index.html' as root
if (path.endsWith('index.html')) {
const p = path.slice(0, -'index.html'.length);
return p === '' ? '/' : p;
}
return path.endsWith('/') ? path : path;
}
function setActiveNavLink() {
const links = document.querySelectorAll('#nav-links a[href]');
if (!links.length) return;
const currentPath = normalizePath(window.location.pathname);
const currentHash = window.location.hash || '';
let bestMatch = null;
let bestScore = -1;
links.forEach(link => {
const href = link.getAttribute('href');
let linkUrl;
try {
linkUrl = new URL(href, document.location.href);
} catch (e) {
return;
}
const linkPath = normalizePath(linkUrl.pathname);
const linkHash = linkUrl.hash || '';
let score = 0;
if (linkPath === currentPath) score += 2;
if (linkHash && linkHash === currentHash) score += 4;
// Prefer exact hash matches, then exact path matches
if (score > bestScore) {
bestScore = score;
bestMatch = link;
}
});
// Fallback: if on root (/) and there is an index link, pick it
if (!bestMatch) {
links.forEach(link => {
if (!bestMatch && /index\.html$|\/$/.test(link.getAttribute('href') || '')) {
bestMatch = link;
}
});
}
links.forEach(l => { l.classList.remove('active'); l.removeAttribute('aria-current'); });
if (bestMatch) { bestMatch.classList.add('active'); bestMatch.setAttribute('aria-current', 'page'); }
}
// initialize and listen for navigation/hash changes
setActiveNavLink();
window.addEventListener('hashchange', setActiveNavLink);
window.addEventListener('popstate', setActiveNavLink);
// Home page: observe sections so #apropos and #contact become active when visible,
// and revert to "Accueil" when neither section is in view.
let homeSectionObserver = null;
let homeVisibility = {};
function initHomeSectionObserver() {
// clean existing observer
if (homeSectionObserver) {
homeSectionObserver.disconnect();
homeSectionObserver = null;
homeVisibility = {};
}
// Instead of requiring a strict '/' path, detect whether the current document
// actually contains any sections referenced by nav links (e.g. #apropos, #contact).
const links = Array.from(document.querySelectorAll('#nav-links a[href]'));
const sectionLinks = links.filter(link => {
try {
const u = new URL(link.getAttribute('href'), document.location.href);
if (!u.hash) return false;
return !!document.querySelector(u.hash);
} catch (e) {
return false;
}
});
if (!sectionLinks.length) return;
const sections = sectionLinks.map(link => {
const hash = new URL(link.getAttribute('href'), document.location.href).hash;
const el = document.querySelector(hash);
return el ? { id: el.id, el, link } : null;
}).filter(Boolean);
if (!sections.length) return;
// setup observer
homeSectionObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
homeVisibility[entry.target.id] = entry.intersectionRatio || 0;
});
// pick the section with highest intersection ratio
let bestId = null; let bestRatio = 0;
Object.entries(homeVisibility).forEach(([id, ratio]) => {
if (ratio > bestRatio) { bestRatio = ratio; bestId = id; }
});
const navLinks = Array.from(document.querySelectorAll('#nav-links a[href]'));
// if a section is sufficiently visible, activate its link
if (bestId && bestRatio > 0.15) {
const targetLink = navLinks.find(l => {
try {
return new URL(l.getAttribute('href'), document.location.href).hash === '#' + bestId;
} catch (e) { return false; }
});
if (targetLink) {
navLinks.forEach(l => { l.classList.remove('active'); l.removeAttribute('aria-current'); });
targetLink.classList.add('active');
targetLink.setAttribute('aria-current', 'page');
return;
}
}
// fallback: set Accueil active when no section is visible
const indexLink = Array.from(navLinks).find(l => /index\.html$|\/$/.test(l.getAttribute('href') || ''));
navLinks.forEach(l => { l.classList.remove('active'); l.removeAttribute('aria-current'); });
if (indexLink) { indexLink.classList.add('active'); indexLink.setAttribute('aria-current', 'page'); }
}, { root: null, threshold: [0, 0.15, 0.3, 0.6] });
sections.forEach(s => {
homeVisibility[s.id] = 0;
homeSectionObserver.observe(s.el);
});
}
// initialize observer on load and update when navigation changes
initHomeSectionObserver();
window.addEventListener('resize', () => { if (homeSectionObserver) initHomeSectionObserver(); });
// re-init observer when URL path changes (i.e., leave/enter index)
const originalSetActive = setActiveNavLink;
function wrappedSetActive() {
// if leaving index, disconnect observer
const path = normalizePath(window.location.pathname);
if (path !== '/') {
if (homeSectionObserver) { homeSectionObserver.disconnect(); homeSectionObserver = null; homeVisibility = {}; }
originalSetActive();
return;
}
// on index, ensure observer is active and let it control the highlight where possible
initHomeSectionObserver();
originalSetActive();
}
// replace handlers so navigation updates use our wrapped variant
window.removeEventListener('hashchange', setActiveNavLink);
window.removeEventListener('popstate', setActiveNavLink);
window.addEventListener('hashchange', wrappedSetActive);
window.addEventListener('popstate', wrappedSetActive);
// Reveal on scroll
const revealElements = document.querySelectorAll('.reveal');
function revealOnScroll() {
if (!revealElements.length) return;
revealElements.forEach(el => {
const elementTop = el.getBoundingClientRect().top;
if (elementTop < window.innerHeight - 100) {
el.classList.add('active');
}
});
}
window.addEventListener('scroll', revealOnScroll);
window.addEventListener('load', revealOnScroll);
// Contact form (if exists)
const contactForm = document.getElementById('contact-form');
if (contactForm) {
contactForm.addEventListener('submit', function (e) {
e.preventDefault();
alert('Merci pour votre message ! Nous vous répondrons très bientôt.');
this.reset();
});
}
});

209
assets/js/pages/404.js Normal file
View file

@ -0,0 +1,209 @@
feather.replace();
// Particles
const particlesContainer = document.getElementById('particles');
for (let i = 0; i < 30; i++) {
const particle = document.createElement('div');
particle.classList.add('particle');
particle.style.left = Math.random() * 100 + '%';
particle.style.animationDelay = Math.random() * 10 + 's';
particle.style.animationDuration = (Math.random() * 5 + 8) + 's';
particle.style.width = (Math.random() * 4 + 2) + 'px';
particle.style.height = particle.style.width;
particlesContainer.appendChild(particle);
}
// Easter Egg: Konami Code (desktop) ou 10 taps (mobile)
const konamiCode = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'b', 'a'];
let konamiIndex = 0;
let tapCount = 0;
let tapTimer = null;
const REQUIRED_TAPS = 10;
const errorCodeEl = document.querySelector('.error-code');
// Desktop: Konami Code
document.addEventListener('keydown', (e) => {
if (e.key === konamiCode[konamiIndex]) {
konamiIndex++;
if (konamiIndex === konamiCode.length) {
startPortalTransition();
konamiIndex = 0;
}
} else {
konamiIndex = 0;
}
});
// Mobile: 10 taps
function handleTap(e) {
if (e.type === 'touchend') e.preventDefault();
tapCount++;
errorCodeEl.style.transform = 'scale(0.95)';
setTimeout(() => errorCodeEl.style.transform = '', 100);
if (tapCount >= 3 && tapCount < REQUIRED_TAPS) {
showTapCounter(tapCount, REQUIRED_TAPS);
}
if (tapTimer) clearTimeout(tapTimer);
tapTimer = setTimeout(() => { tapCount = 0; hideTapCounter(); }, 500);
if (tapCount >= REQUIRED_TAPS) {
tapCount = 0;
hideTapCounter();
startPortalTransition();
}
}
function showTapCounter(current, total) {
let counter = document.getElementById('tap-counter');
if (!counter) {
counter = document.createElement('div');
counter.id = 'tap-counter';
counter.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);background:rgba(139,58,98,0.9);color:white;padding:0.5rem 1rem;border-radius:20px;font-size:0.85rem;font-weight:600;z-index:1000;';
document.body.appendChild(counter);
}
counter.textContent = current + ' / ' + total + ' 🔓';
}
function hideTapCounter() {
const counter = document.getElementById('tap-counter');
if (counter) counter.remove();
}
errorCodeEl.addEventListener('click', handleTap);
errorCodeEl.addEventListener('touchend', handleTap);
// Portal Transition
function startPortalTransition() {
const transition = document.createElement('div');
transition.id = 'portal-transition';
transition.innerHTML = '<div class="warp-container"><div class="warp-tunnel"></div><div class="warp-stars"></div><div class="warp-center"><div class="warp-ring"></div><div class="warp-ring"></div><div class="warp-ring"></div></div><div class="warp-flash"></div></div><div class="warp-text">ENTRÉE DANS LA MATRICE</div>';
document.body.appendChild(transition);
const style = document.createElement('style');
style.id = 'transition-styles';
style.textContent = '#portal-transition{position:fixed;inset:0;z-index:10000;background:#000;display:flex;align-items:center;justify-content:center;overflow:hidden}.warp-container{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;perspective:500px}.warp-tunnel{position:absolute;width:200vmax;height:200vmax;background:repeating-conic-gradient(from 0deg,transparent 0deg 10deg,rgba(139,58,98,0.1) 10deg 20deg);animation:tunnelSpin 2s linear infinite,tunnelZoom 3s ease-in forwards}@keyframes tunnelSpin{to{transform:rotate(360deg)}}@keyframes tunnelZoom{0%{transform:rotate(0)scale(1);opacity:0}20%{opacity:1}100%{transform:rotate(720deg)scale(20);opacity:0}}.warp-stars{position:absolute;inset:0;background-image:radial-gradient(2px 2px at 20px 30px,#fff,transparent),radial-gradient(2px 2px at 40px 70px,rgba(255,255,255,0.8),transparent),radial-gradient(1px 1px at 90px 40px,#fff,transparent);background-size:350px 200px;animation:starsWarp 3s ease-in forwards}@keyframes starsWarp{0%{transform:scale(1);opacity:0}30%{opacity:1}100%{transform:scale(50);opacity:0}}.warp-center{position:relative;width:150px;height:150px;animation:centerPulse 0.5s ease-in-out infinite,centerGrow 2.5s ease-in forwards}@keyframes centerPulse{0%,100%{filter:brightness(1)}50%{filter:brightness(1.5)}}@keyframes centerGrow{0%{transform:scale(0);opacity:0}20%{transform:scale(1);opacity:1}80%{transform:scale(1);opacity:1}100%{transform:scale(100);opacity:1}}.warp-ring{position:absolute;inset:0;border:4px solid #8b3a62;border-radius:50%;animation:ringPulse 1s ease-in-out infinite;box-shadow:0 0 30px #8b3a62,inset 0 0 30px #8b3a62}.warp-ring:nth-child(2){inset:20px;border-color:#5e2a8b;animation-delay:0.2s;box-shadow:0 0 30px #5e2a8b,inset 0 0 30px #5e2a8b}.warp-ring:nth-child(3){inset:40px;border-color:#d4a5c9;animation-delay:0.4s;box-shadow:0 0 30px #d4a5c9,inset 0 0 30px #d4a5c9}@keyframes ringPulse{0%,100%{transform:scale(1);opacity:1}50%{transform:scale(1.1);opacity:0.8}}.warp-flash{position:absolute;inset:0;background:#fff;opacity:0;animation:flashBang 0.5s ease-out 2.5s forwards}@keyframes flashBang{0%{opacity:0}50%{opacity:1}100%{opacity:0}}.warp-text{position:absolute;font-size:1.5rem;font-weight:700;color:#fff;letter-spacing:0.3rem;z-index:10;opacity:0;animation:textAppear 0.5s ease 0.5s forwards,textFade 0.5s ease 2s forwards}@keyframes textAppear{to{opacity:1}}@keyframes textFade{to{opacity:0;transform:scale(2)}}';
document.head.appendChild(style);
setTimeout(() => {
transition.remove();
style.remove();
launchGameOfLife();
}, 3000);
}
// Game of Life
function launchGameOfLife() {
const gameContainer = document.createElement('div');
gameContainer.id = 'game-of-life';
gameContainer.innerHTML = '<canvas id="gol-canvas"></canvas><div class="gol-ui"><div class="gol-header"><div class="gol-header-text"><h1>🧬 Dimension Secrète</h1><p>Le Jeu de la Vie de Conway</p></div><button id="gol-minimize" class="gol-minimize" title="Réduire"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="icon-min"><line x1="5" y1="12" x2="19" y2="12"/></svg><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="icon-max"><rect x="3" y="3" width="18" height="18" rx="2"/></svg></button></div><div class="gol-body"><div class="gol-description"><p><strong>Règles :</strong> Cellule vivante avec 2-3 voisins survit. Cellule morte avec 3 voisins naît.</p></div><div class="gol-controls"><button id="gol-play" class="gol-btn active" title="Play/Pause"><svg viewBox="0 0 24 24" fill="currentColor" class="icon-pause"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg><svg viewBox="0 0 24 24" fill="currentColor" class="icon-play"><polygon points="5,3 19,12 5,21"/></svg></button><button id="gol-step" class="gol-btn" title="Étape"><svg viewBox="0 0 24 24" fill="currentColor"><polygon points="5,3 15,12 5,21"/><rect x="15" y="3" width="4" height="18"/></svg></button><button id="gol-random" class="gol-btn" title="Aléatoire"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg></button><button id="gol-clear" class="gol-btn" title="Effacer"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button><button id="gol-gliders" class="gol-btn" title="Planeurs"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 2L11 13"/><polygon points="22,2 15,22 11,13 2,9"/></svg></button></div><div class="gol-zoom-row"><button id="gol-zoom-out" class="gol-btn gol-btn-sm">−</button><span id="gol-zoom-val">30px</span><button id="gol-zoom-in" class="gol-btn gol-btn-sm">+</button></div><div class="gol-speed-row"><label>Vitesse: <span id="gol-speed-val">15</span> fps</label><input type="range" id="gol-speed" min="1" max="60" value="15"></div><div class="gol-stats"><div class="gol-stat"><span id="gol-gen">0</span><label>Génération</label></div><div class="gol-stat"><span id="gol-pop">0</span><label>Population</label></div><div class="gol-stat"><span id="gol-dim">0×0</span><label>Grille</label></div></div></div><button id="gol-exit" class="gol-exit">✕ Quitter</button></div>';
document.body.appendChild(gameContainer);
const gameStyles = document.createElement('style');
gameStyles.id = 'gol-styles';
gameStyles.textContent = '#game-of-life{position:fixed;inset:0;z-index:10000;background:#050508;animation:golIn 0.5s ease}@keyframes golIn{from{opacity:0}to{opacity:1}}#gol-canvas{position:absolute;inset:0;width:100%;height:100%;cursor:crosshair}.gol-ui{position:absolute;top:20px;left:20px;background:rgba(10,10,15,0.95);backdrop-filter:blur(10px);border:1px solid rgba(139,58,98,0.3);border-radius:16px;padding:1rem;min-width:280px;box-shadow:0 20px 60px rgba(0,0,0,0.5)}.gol-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:0.75rem}.gol-header-text h1{font-size:1.1rem;margin-bottom:0.1rem;background:linear-gradient(135deg,#d4a5c9,#8b3a62);-webkit-background-clip:text;-webkit-text-fill-color:transparent}.gol-header-text p{font-size:0.7rem;color:#6b6b7b}.gol-minimize{width:32px;height:32px;border:1px solid rgba(255,255,255,0.1);background:rgba(255,255,255,0.05);border-radius:8px;color:#b8b8c8;cursor:pointer;display:flex;align-items:center;justify-content:center}.gol-minimize:hover{background:rgba(139,58,98,0.3);border-color:#8b3a62;color:#fff}.gol-minimize svg{width:16px;height:16px}.gol-minimize .icon-max{display:none}.gol-ui.minimized .gol-body,.gol-ui.minimized .gol-exit{display:none}.gol-ui.minimized .gol-minimize .icon-min{display:none}.gol-ui.minimized .gol-minimize .icon-max{display:block}.gol-ui.minimized{min-width:auto}.gol-body{display:flex;flex-direction:column;gap:0.75rem}.gol-description{background:rgba(139,58,98,0.1);border:1px solid rgba(139,58,98,0.2);border-radius:8px;padding:0.5rem}.gol-description p{font-size:0.7rem;color:#b8b8c8;margin:0}.gol-description strong{color:#d4a5c9}.gol-controls{display:flex;gap:0.4rem;flex-wrap:wrap}.gol-btn{width:40px;height:40px;border:1px solid rgba(255,255,255,0.1);background:rgba(255,255,255,0.05);border-radius:8px;color:#b8b8c8;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all 0.2s}.gol-btn:hover{background:rgba(139,58,98,0.3);border-color:#8b3a62;color:#fff;transform:translateY(-2px)}.gol-btn svg{width:18px;height:18px}.gol-btn.active{background:linear-gradient(135deg,#8b3a62,#5e2a8b);border-color:transparent;color:#fff}.gol-btn .icon-play{display:none}.gol-btn .icon-pause{display:block}.gol-btn:not(.active) .icon-play{display:block}.gol-btn:not(.active) .icon-pause{display:none}.gol-btn-sm{width:32px;height:32px;font-size:1.2rem;font-weight:bold}.gol-zoom-row,.gol-speed-row{display:flex;align-items:center;gap:0.5rem}.gol-zoom-row span{color:#d4a5c9;font-family:monospace;font-size:0.85rem;min-width:40px;text-align:center}.gol-speed-row{flex-direction:column;align-items:stretch}.gol-speed-row label{font-size:0.75rem;color:#b8b8c8}.gol-speed-row label span{color:#d4a5c9;font-family:monospace}.gol-speed-row input{width:100%;height:6px;-webkit-appearance:none;background:rgba(255,255,255,0.1);border-radius:3px;cursor:pointer}.gol-speed-row input::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;background:linear-gradient(135deg,#8b3a62,#5e2a8b);border-radius:50%;cursor:pointer}.gol-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:0.5rem}.gol-stat{text-align:center;padding:0.4rem;background:rgba(255,255,255,0.03);border-radius:6px}.gol-stat span{font-size:1rem;font-weight:700;color:#d4a5c9;font-family:monospace;display:block}.gol-stat label{font-size:0.6rem;color:#6b6b7b;text-transform:uppercase}.gol-exit{width:100%;margin-top:0.5rem;padding:0.6rem;background:transparent;border:1px solid rgba(255,255,255,0.1);border-radius:8px;color:#6b6b7b;font-size:0.8rem;cursor:pointer;transition:all 0.2s}.gol-exit:hover{background:rgba(255,50,50,0.2);border-color:rgba(255,50,50,0.5);color:#ff6b6b}@media(max-width:768px){.gol-ui{top:auto;bottom:0;left:0;right:0;border-radius:16px 16px 0 0;max-height:50vh;overflow-y:auto;min-width:auto}.gol-header-text p,.gol-description{display:none}.gol-ui.minimized{max-height:none}}@media(max-width:480px){.gol-ui{padding:0.75rem}.gol-btn{width:36px;height:36px}.gol-btn-sm{width:28px;height:28px}}';
document.head.appendChild(gameStyles);
const canvas = document.getElementById('gol-canvas');
const ctx = canvas.getContext('2d');
const MAX_CELLS = 150, MIN_SIZE = 4, MAX_SIZE = 30;
let cellSize = MAX_SIZE, minCellSize = MIN_SIZE, cols, rows, grid, nextGrid;
let isRunning = true, generation = 0, speed = 15, lastTime = 0, isDrawing = false, drawValue = 1;
function initCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
minCellSize = Math.max(MIN_SIZE, Math.ceil(canvas.width / MAX_CELLS), Math.ceil(canvas.height / MAX_CELLS));
cellSize = Math.max(minCellSize, Math.min(MAX_SIZE, cellSize));
cols = Math.ceil(canvas.width / cellSize);
rows = Math.ceil(canvas.height / cellSize);
grid = Array(cols).fill(null).map(() => Array(rows).fill(0));
nextGrid = Array(cols).fill(null).map(() => Array(rows).fill(0));
updateUI();
}
function addGlider(sx, sy, dir) {
const p = [[[0,1],[1,2],[2,0],[2,1],[2,2]],[[0,1],[1,0],[2,0],[2,1],[2,2]],[[0,0],[0,1],[0,2],[1,2],[2,1]],[[0,0],[0,1],[0,2],[1,0],[2,1]]];
p[dir % 4].forEach(([dx, dy]) => { const x = (sx + dx) % cols, y = (sy + dy) % rows; if (x >= 0 && x < cols && y >= 0 && y < rows) grid[x][y] = 1; });
}
function addGliders(n) {
grid = Array(cols).fill(null).map(() => Array(rows).fill(0));
for (let i = 0; i < n; i++) addGlider(Math.floor(Math.random() * (cols - 10)) + 5, Math.floor(Math.random() * (rows - 10)) + 5, Math.floor(Math.random() * 4));
generation = 0; updateUI();
}
function randomize() { for (let x = 0; x < cols; x++) for (let y = 0; y < rows; y++) grid[x][y] = Math.random() > 0.7 ? 1 : 0; generation = 0; updateUI(); }
function clear() { for (let x = 0; x < cols; x++) for (let y = 0; y < rows; y++) grid[x][y] = 0; generation = 0; updateUI(); }
function step() {
for (let x = 0; x < cols; x++) for (let y = 0; y < rows; y++) {
let n = 0;
for (let i = -1; i <= 1; i++) for (let j = -1; j <= 1; j++) if (i || j) n += grid[(x + i + cols) % cols][(y + j + rows) % rows];
nextGrid[x][y] = grid[x][y] ? (n === 2 || n === 3 ? 1 : 0) : (n === 3 ? 1 : 0);
}
[grid, nextGrid] = [nextGrid, grid]; generation++; updateUI();
}
function draw() {
ctx.fillStyle = '#0a0a0f'; ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = 'rgba(139,58,98,0.1)'; ctx.lineWidth = 0.5;
for (let x = 0; x <= cols * cellSize; x += cellSize) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, rows * cellSize); ctx.stroke(); }
for (let y = 0; y <= rows * cellSize; y += cellSize) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(cols * cellSize, y); ctx.stroke(); }
ctx.shadowColor = 'rgba(139,58,98,0.5)'; ctx.shadowBlur = 8;
for (let x = 0; x < cols; x++) for (let y = 0; y < rows; y++) if (grid[x][y]) {
const g = ctx.createRadialGradient(x * cellSize + cellSize / 2, y * cellSize + cellSize / 2, 0, x * cellSize + cellSize / 2, y * cellSize + cellSize / 2, cellSize);
g.addColorStop(0, '#d4a5c9'); g.addColorStop(0.5, '#8b3a62'); g.addColorStop(1, '#5e2a8b');
ctx.fillStyle = g; ctx.fillRect(x * cellSize + 1, y * cellSize + 1, cellSize - 2, cellSize - 2);
}
ctx.shadowBlur = 0;
}
function updateUI() {
let pop = 0; for (let x = 0; x < cols; x++) for (let y = 0; y < rows; y++) pop += grid[x][y];
document.getElementById('gol-gen').textContent = generation;
document.getElementById('gol-pop').textContent = pop;
document.getElementById('gol-dim').textContent = cols + '×' + rows;
document.getElementById('gol-zoom-val').textContent = cellSize + 'px';
document.getElementById('gol-speed-val').textContent = speed;
}
function handleZoom(d) {
const old = cellSize; cellSize = Math.max(minCellSize, Math.min(MAX_SIZE, cellSize + d));
if (cellSize !== old) { const og = grid, oc = cols, or = rows; initCanvas(); for (let x = 0; x < Math.min(oc, cols); x++) for (let y = 0; y < Math.min(or, rows); y++) grid[x][y] = og[x][y]; updateUI(); }
}
function getCell(e) {
const r = canvas.getBoundingClientRect(), cx = e.touches ? e.touches[0].clientX : e.clientX, cy = e.touches ? e.touches[0].clientY : e.clientY;
return { x: Math.max(0, Math.min(cols - 1, Math.floor((cx - r.left) / cellSize))), y: Math.max(0, Math.min(rows - 1, Math.floor((cy - r.top) / cellSize))) };
}
function gameLoop(t) { if (isRunning && t - lastTime > 1000 / speed) { step(); lastTime = t; } draw(); requestAnimationFrame(gameLoop); }
canvas.addEventListener('mousedown', e => { isDrawing = true; const c = getCell(e); drawValue = grid[c.x][c.y] ? 0 : 1; grid[c.x][c.y] = drawValue; });
canvas.addEventListener('mousemove', e => { if (isDrawing) { const c = getCell(e); grid[c.x][c.y] = drawValue; } });
canvas.addEventListener('mouseup', () => isDrawing = false);
canvas.addEventListener('mouseleave', () => isDrawing = false);
canvas.addEventListener('touchstart', e => { e.preventDefault(); isDrawing = true; const c = getCell(e); drawValue = grid[c.x][c.y] ? 0 : 1; grid[c.x][c.y] = drawValue; }, { passive: false });
canvas.addEventListener('touchmove', e => { e.preventDefault(); if (isDrawing) { const c = getCell(e); grid[c.x][c.y] = drawValue; } }, { passive: false });
canvas.addEventListener('touchend', () => isDrawing = false);
canvas.addEventListener('wheel', e => { e.preventDefault(); handleZoom(e.deltaY > 0 ? -1 : 1); });
document.getElementById('gol-play').addEventListener('click', function() { isRunning = !isRunning; this.classList.toggle('active', isRunning); });
document.getElementById('gol-step').addEventListener('click', () => { if (!isRunning) step(); });
document.getElementById('gol-random').addEventListener('click', randomize);
document.getElementById('gol-clear').addEventListener('click', clear);
document.getElementById('gol-gliders').addEventListener('click', () => addGliders(3));
document.getElementById('gol-zoom-in').addEventListener('click', () => handleZoom(2));
document.getElementById('gol-zoom-out').addEventListener('click', () => handleZoom(-2));
document.getElementById('gol-speed').addEventListener('input', function() { speed = parseInt(this.value); updateUI(); });
document.getElementById('gol-minimize').addEventListener('click', () => document.querySelector('.gol-ui').classList.toggle('minimized'));
document.getElementById('gol-exit').addEventListener('click', () => { document.getElementById('game-of-life').remove(); document.getElementById('gol-styles').remove(); });
window.addEventListener('resize', () => { const og = grid, oc = cols, or = rows; initCanvas(); for (let x = 0; x < Math.min(oc, cols); x++) for (let y = 0; y < Math.min(or, rows); y++) grid[x][y] = og[x][y]; updateUI(); });
initCanvas();
addGliders(3);
requestAnimationFrame(gameLoop);
}

31
assets/js/pages/legal.js Normal file
View file

@ -0,0 +1,31 @@
// assets/js/pages/legal.js - minimal fallback for legal pages
document.addEventListener('DOMContentLoaded', () => {
// replace feather icons if available (use centralized loader if present)
if (typeof window.ensureFeather === 'function') {
window.ensureFeather().then(() => { try { if (window.feather && feather.replace) feather.replace(); } catch (e) {} }).catch(()=>{});
} else if (window.feather && feather.replace) {
try { feather.replace(); } catch (e) { /* ignore */ }
}
// theme fallback (will be overwritten by assets/js/main.js if present)
const html = document.documentElement;
if (!html.getAttribute('data-theme')) html.setAttribute('data-theme', localStorage.getItem('theme') || 'dark');
const themeToggle = document.getElementById('theme-toggle');
if (themeToggle) themeToggle.addEventListener('click', () => {
const newTheme = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
html.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
});
// navbar scrolled
const navbar = document.getElementById('navbar');
if (navbar) window.addEventListener('scroll', () => navbar.classList.toggle('scrolled', window.scrollY > 50));
// mobile toggle (minimal)
const mobileToggle = document.getElementById('mobile-toggle');
const navLinks = document.getElementById('nav-links');
if (mobileToggle && navLinks) mobileToggle.addEventListener('click', () => {
mobileToggle.classList.toggle('active');
navLinks.classList.toggle('active');
});
});

View file

@ -0,0 +1,228 @@
// Page script for /pages/realisations.html
document.addEventListener('DOMContentLoaded', async () => {
const dataUrl = '../assets/data/projects.json';
let allProjects = [];
try {
const res = await fetch(dataUrl);
if (!res.ok) throw new Error(res.statusText);
allProjects = await res.json();
} catch (err) {
console.error('Failed to load projects data:', err);
return;
}
const ITEMS_PER_PAGE = 9;
let currentPage = 1;
let currentFilter = 'all';
const portfolioGrid = document.getElementById('portfolio-grid');
const paginationContainer = document.getElementById('pagination');
const filterTabs = document.querySelectorAll('.filter-tab');
const modalOverlay = document.getElementById('project-modal-overlay');
const modalClose = document.getElementById('project-modal-close');
const gallerySlides = document.getElementById('gallery-slides');
const galleryPrev = document.getElementById('gallery-prev');
const galleryNext = document.getElementById('gallery-next');
const modalTitle = document.getElementById('modal-title');
const modalCategory = document.getElementById('modal-category');
const modalDesc = document.getElementById('modal-desc');
const modalMeta = document.getElementById('modal-meta');
const modalFeatures = document.getElementById('modal-features');
const modalTech = document.getElementById('modal-tech');
const modalStats = document.getElementById('modal-stats');
function getFilteredProjects() {
if (currentFilter === 'all') return [...allProjects];
return allProjects.filter(p => p.category === currentFilter);
}
function getTotalPages(filtered) {
return Math.ceil(filtered.length / ITEMS_PER_PAGE) || 1;
}
function getProjectsForPage(filtered, page) {
const start = (page - 1) * ITEMS_PER_PAGE;
return filtered.slice(start, start + ITEMS_PER_PAGE);
}
function getCategoryLabel(category) {
const labels = { 'bot': 'Bot Discord', 'map': 'Fortnite Map', 'graphisme': 'Graphisme' };
return labels[category] || category;
}
function renderProjects() {
const filtered = getFilteredProjects();
const projects = getProjectsForPage(filtered, currentPage);
portfolioGrid.innerHTML = projects.map(project => `
<div class="portfolio-item reveal" data-project-id="${project.id}">
<div class="portfolio-bg" style="background: ${project.gradient};"></div>
<div class="portfolio-content">
<span class="portfolio-tag">${getCategoryLabel(project.category)}</span>
<h4>${project.title}</h4>
<p>${project.desc.substring(0, 80)}...</p>
</div>
</div>
`).join('');
// activate reveals & icons
setTimeout(() => {
document.querySelectorAll('.portfolio-item.reveal').forEach(el => el.classList.add('active'));
}, 80);
// ensure icons are replaced for newly injected content
if (window.feather && feather.replace) {
try { feather.replace(); } catch (e) {}
}
}
function renderPagination() {
const filtered = getFilteredProjects();
const totalPages = getTotalPages(filtered);
if (totalPages <= 1) {
paginationContainer.innerHTML = '';
return;
}
let html = `<button class="pagination-btn pagination-arrow" data-page="${Math.max(1, currentPage - 1)}" ${currentPage === 1 ? 'disabled' : ''}><i data-feather="chevron-left"></i></button>`;
const maxVisible = 5;
let startPage = Math.max(1, currentPage - Math.floor(maxVisible / 2));
let endPage = Math.min(totalPages, startPage + maxVisible - 1);
if (endPage - startPage < maxVisible - 1) startPage = Math.max(1, endPage - maxVisible + 1);
if (startPage > 1) {
html += `<button class="pagination-btn" data-page="1">1</button>`;
if (startPage > 2) html += `<span class="pagination-ellipsis">...</span>`;
}
for (let i = startPage; i <= endPage; i++) {
html += `<button class="pagination-btn ${i === currentPage ? 'active' : ''}" data-page="${i}">${i}</button>`;
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) html += `<span class="pagination-ellipsis">...</span>`;
html += `<button class="pagination-btn" data-page="${totalPages}">${totalPages}</button>`;
}
html += `<button class="pagination-btn pagination-arrow" data-page="${Math.min(totalPages, currentPage + 1)}" ${currentPage === totalPages ? 'disabled' : ''}><i data-feather="chevron-right"></i></button>`;
const startItem = (currentPage - 1) * ITEMS_PER_PAGE + 1;
const endItem = Math.min(currentPage * ITEMS_PER_PAGE, filtered.length);
html += `<div class="pagination-info">Affichage ${startItem}-${endItem} sur ${filtered.length} projets</div>`;
paginationContainer.innerHTML = html;
if (window.feather && feather.replace) {
try { feather.replace(); } catch (e) {}
}
}
function goToPage(page) {
const filtered = getFilteredProjects();
const totalPages = getTotalPages(filtered);
if (page < 1 || page > totalPages) return;
currentPage = page;
renderProjects();
renderPagination();
portfolioGrid.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function openProjectModal(projectId) {
const project = allProjects.find(p => p.id === Number(projectId));
if (!project) return;
modalCategory.textContent = getCategoryLabel(project.category);
modalTitle.textContent = project.title;
modalDesc.textContent = project.desc;
modalMeta.innerHTML = `<span><i data-feather="calendar"></i> ${project.year}</span> <span><i data-feather="clock"></i> ${project.duration}</span>`;
modalFeatures.innerHTML = project.features.map(f => `<div class="project-feature-item">${f}</div>`).join('');
modalTech.innerHTML = project.tech.map(t => `<div class="project-tech-item">${t}</div>`).join('');
modalStats.innerHTML = project.stats.map(s => `
<div class="project-stat-card">
<div class="project-stat-card-value">${s.value}</div>
<div class="project-stat-card-label">${s.label}</div>
</div>
`).join('');
// gallery slides (use gradient as placeholder slides like original)
gallerySlides.innerHTML = `
<div class="project-gallery-slide" style="background: ${project.gradient};"></div>
<div class="project-gallery-slide" style="background: ${project.gradient}; filter: hue-rotate(30deg);"></div>
<div class="project-gallery-slide" style="background: ${project.gradient}; filter: hue-rotate(-30deg);"></div>
`;
// reset gallery state
galleryIndex = 0;
updateGallery();
if (modalOverlay) modalOverlay.classList.add('active');
if (window.feather && feather.replace) {
try { feather.replace(); } catch (e) {}
}
}
function closeProjectModal() {
if (modalOverlay) modalOverlay.classList.remove('active');
}
// Gallery helpers
let galleryIndex = 0;
function updateGallery() {
const slides = gallerySlides ? Array.from(gallerySlides.children) : [];
slides.forEach((s, i) => {
s.style.transform = `translateX(${(i - galleryIndex) * 100}%)`;
s.classList.toggle('active', i === galleryIndex);
});
}
function prevSlide() { galleryIndex = Math.max(0, galleryIndex - 1); updateGallery(); }
function nextSlide() { const slides = gallerySlides ? gallerySlides.children.length : 0; galleryIndex = Math.min(slides - 1, galleryIndex + 1); updateGallery(); }
// Event bindings
filterTabs.forEach(tab => tab.addEventListener('click', (e) => {
filterTabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
currentFilter = tab.dataset.filter || 'all';
currentPage = 1;
renderProjects();
renderPagination();
}));
paginationContainer.addEventListener('click', (e) => {
const btn = e.target.closest('button[data-page]');
if (!btn) return;
const page = Number(btn.getAttribute('data-page'));
goToPage(page);
});
portfolioGrid.addEventListener('click', (e) => {
const item = e.target.closest('.portfolio-item');
if (!item) return;
const id = item.getAttribute('data-project-id');
openProjectModal(id);
});
if (modalClose) modalClose.addEventListener('click', closeProjectModal);
if (modalOverlay) modalOverlay.addEventListener('click', (e) => { if (e.target === modalOverlay) closeProjectModal(); });
window.addEventListener('keydown', (e) => { if (e.key === 'Escape' && modalOverlay.classList.contains('active')) closeProjectModal(); });
if (galleryPrev) galleryPrev.addEventListener('click', prevSlide);
if (galleryNext) galleryNext.addEventListener('click', nextSlide);
// Expose for compatibility
window.openProjectModal = openProjectModal;
window.goToPage = goToPage;
// Initial render
renderProjects();
renderPagination();
});

View file

@ -0,0 +1,57 @@
// Services page scripts (moved from inline in pages/services.html)
// Depends on ../main.js which initializes feather and other globals
(function() {
document.addEventListener('DOMContentLoaded', () => {
// Timeline Animation
const timeline = document.getElementById('process-timeline');
const steps = document.querySelectorAll('.process-step');
function animateTimeline() {
if (!timeline) return;
const timelineTop = timeline.getBoundingClientRect().top;
if (timelineTop < window.innerHeight - 200) {
timeline.classList.add('animated');
// Animate steps sequentially
steps.forEach((step, index) => {
setTimeout(() => {
step.classList.add('active');
}, index * 400);
});
}
}
window.addEventListener('scroll', animateTimeline);
window.addEventListener('load', animateTimeline);
// Service Tabs
const tabBtns = document.querySelectorAll('.service-tab-btn');
const tabContents = document.querySelectorAll('.service-tab-content');
if (tabBtns.length && tabContents.length) {
tabBtns.forEach(btn => {
btn.addEventListener('click', () => {
const tabId = btn.dataset.tab;
// Update buttons
tabBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// Update content
tabContents.forEach(content => {
content.classList.remove('active');
if (content.id === 'tab-' + tabId) {
content.classList.add('active');
}
});
// Re-init feather icons for new content (safe if feather is loaded)
if (window.feather) window.feather.replace();
});
});
}
});
})();