update last version of the website
331
assets/css/404.css
Normal 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; }
|
||||
}
|
||||
46
assets/css/legal.css
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/* assets/css/legal.css - styles pour les pages légales */
|
||||
|
||||
/* Hero */
|
||||
.page-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 1rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.page-hero .hero-content { max-width: 1100px; text-align: center; }
|
||||
.page-hero .hero-content h1 { margin-bottom: 0.5rem; }
|
||||
.page-hero .hero-content p { margin: 0; }
|
||||
|
||||
/* Legal content */
|
||||
.legal-content { padding: 2.5rem 1rem 4rem; }
|
||||
.legal-container { max-width: 900px; margin: 0 auto; }
|
||||
.legal-section { margin-bottom: 2rem; }
|
||||
.legal-section h2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.legal-section h2 svg { width: 18px; height: 18px; flex-shrink: 0; }
|
||||
.legal-card {
|
||||
background: var(--card-bg);
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px var(--shadow-color);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.legal-card p,
|
||||
.legal-card ul {
|
||||
margin: 0 0 0.75rem 0;
|
||||
line-height: 1.7;
|
||||
font-size: 1rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.legal-card p:last-child,
|
||||
.legal-card ul:last-child { margin-bottom: 0; }
|
||||
.legal-card ul { padding-left: 1.5rem; }
|
||||
.legal-card ul li { margin-bottom: 0.25rem; }
|
||||
4353
assets/css/main.css
Normal file
78
assets/data/faq.json
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
[
|
||||
{
|
||||
"id": "delais",
|
||||
"icon": "clock",
|
||||
"title": "Délais & Process",
|
||||
"questions": [
|
||||
{
|
||||
"q": "Quels sont les délais habituels pour un bot Discord ?",
|
||||
"a": "Un bot Discord RP complet demande généralement entre 2 et 4 semaines selon la complexité des systèmes (économie, combat, inventaire…). Nous établissons un planning détaillé dès la validation du devis."
|
||||
},
|
||||
{
|
||||
"q": "Et pour une map Fortnite ?",
|
||||
"a": "La création d'une map UEFN prend entre 3 et 6 semaines. Ce délai inclut le design, la construction, les scripts Verse et les tests de performance. Des points réguliers sont organisés pour valider chaque étape."
|
||||
},
|
||||
{
|
||||
"q": "Comment se déroule le processus du début à la fin ?",
|
||||
"a": "Notre processus se déroule en 4 étapes : <strong>Contact</strong> (échange sur vos besoins), <strong>Devis</strong> (proposition détaillée et validation), <strong>Création</strong> (développement avec suivi régulier) et <strong>Livraison</strong> (tests, ajustements et remise des fichiers). Vous êtes impliqué à chaque étape."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "tarifs",
|
||||
"icon": "dollar-sign",
|
||||
"title": "Tarifs & Devis",
|
||||
"questions": [
|
||||
{
|
||||
"q": "Comment sont calculés vos tarifs ?",
|
||||
"a": "Chaque projet est unique, nos tarifs sont donc établis sur devis selon la complexité, le nombre de fonctionnalités et les délais souhaités. Le devis est gratuit, détaillé et sans engagement."
|
||||
},
|
||||
{
|
||||
"q": "Le consulting est-il vraiment gratuit ?",
|
||||
"a": "Oui, la première consultation est entièrement gratuite et sans engagement. Elle permet de cerner vos besoins, évaluer votre projet et vous orienter vers la solution la plus adaptée, qu'elle soit payante ou non."
|
||||
},
|
||||
{
|
||||
"q": "Y a-t-il des frais cachés ?",
|
||||
"a": "Non. Le devis que nous soumettons est exhaustif. Les éventuels ajouts de fonctionnalités en cours de projet font l'objet d'un avenant écrit et validé par les deux parties avant toute réalisation."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "revisions",
|
||||
"icon": "refresh-cw",
|
||||
"title": "Révisions & Support",
|
||||
"questions": [
|
||||
{
|
||||
"q": "Que signifient les « révisions illimitées* » en graphisme ?",
|
||||
"a": "Les révisions illimitées s'appliquent aux ajustements dans le cadre du brief initial : couleurs, proportions, typo, espacement. Elles ne couvrent pas les changements de direction créative complète (nouveau concept, refonte totale), qui sont facturés séparément."
|
||||
},
|
||||
{
|
||||
"q": "Que se passe-t-il si mon bot plante après livraison ?",
|
||||
"a": "Un mois de support est inclus après chaque livraison. Durant cette période, tout bug lié à notre code est corrigé gratuitement et en priorité. Passé ce délai, un contrat de maintenance peut être négocié."
|
||||
},
|
||||
{
|
||||
"q": "Puis-je modifier moi-même le projet après livraison ?",
|
||||
"a": "Oui, vous recevez les fichiers sources complets. Nous livrons également une documentation pour vous permettre d'effectuer des modifications de base. Pour des évolutions plus complexes, notre équipe reste disponible."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "technique",
|
||||
"icon": "code",
|
||||
"title": "Technique",
|
||||
"questions": [
|
||||
{
|
||||
"q": "Quelles technologies utilisez-vous pour les bots Discord ?",
|
||||
"a": "Nos bots sont développés principalement en Python avec discord.py, couplés à une base de données MySQL. Pour l'hébergement, nous pouvons vous guider vers des solutions adaptées (VPS, serveur dédié) ou gérer l'hébergement pour vous."
|
||||
},
|
||||
{
|
||||
"q": "Les maps Fortnite sont-elles compatibles avec les dernières mises à jour du jeu ?",
|
||||
"a": "Oui. Nous développons exclusivement via UEFN (Unreal Editor for Fortnite), l'outil officiel d'Epic Games, ce qui garantit une compatibilité optimale et l'accès aux dernières fonctionnalités disponibles."
|
||||
},
|
||||
{
|
||||
"q": "Pouvez-vous reprendre un projet existant ?",
|
||||
"a": "Oui, nous réalisons des audits de projets existants. Après analyse du code ou de la map, nous vous soumettons un rapport et un plan d'action. C'est d'ailleurs inclus dans notre offre de consulting gratuit."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
119
assets/data/partners.json
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
[
|
||||
{
|
||||
"name": "victorias-vice",
|
||||
"displayName": " Victoria's Vice",
|
||||
"description": "Serveur RP whitelist Fortnite sur thème italien. L'une de nos collaborations les plus abouties.",
|
||||
"longDesc": "Victoria's Vice est un serveur RP whitelist Fortnite immersif basé sur une esthétique italienne unique. OmégaKube a développé l'intégralité de la map UEFN, le bot RP Vicy et l'identité visuelle du projet.",
|
||||
"gradient": "linear-gradient(135deg, #5E2040, #8b3a62)",
|
||||
"since": "2025",
|
||||
"services": [
|
||||
"Sessions RP Victoria's Vice 2"
|
||||
],
|
||||
"stats": [
|
||||
{
|
||||
"icon": "users",
|
||||
"value": "70",
|
||||
"label": "Joueurs max"
|
||||
},
|
||||
{
|
||||
"icon": "home",
|
||||
"value": "30+",
|
||||
"label": "Bâtiments"
|
||||
},
|
||||
{
|
||||
"icon": "play",
|
||||
"value": "30+",
|
||||
"label": "Sessions"
|
||||
}
|
||||
],
|
||||
"social": {
|
||||
"discord": "https://discord.gg/8D564jm2SD",
|
||||
"twitter": "",
|
||||
"instagram": "",
|
||||
"youtube": "https://www.youtube.com/@VictoriasVice",
|
||||
"tiktok": "",
|
||||
"website": "#",
|
||||
"epicgames": "https://www.fortnite.com/@omega_kube_team"
|
||||
},
|
||||
"logo": "assets/images/partners/victorias-vice.png"
|
||||
},
|
||||
{
|
||||
"name": "red-dust-redemption",
|
||||
"displayName": "Red Dust Redemption",
|
||||
"description": "Plongez au cœur du Far West en 1884, dans l’Ouest des États-Unis où chaque choix compte.",
|
||||
"longDesc": "Bienvenue sur Red Dust Redemption RP WL.Plongez au cœur du Far West en 1884, dans l’Ouest des États-Unis où chaque choix compte. Une multitude de possibilités s'offrent à vous. Notamment en travaillant au saloon avec piano en fond, ambiance chaleureuse et rencontres garanties, c'est l’endroit idéal pour ceux qui aiment la convivialité. Et si le danger vous attire, sachez que plusieurs organisations criminelles n’attendent que vous. Si l’aventure vous tente, n’attendez plus et foncez-nous rejoindre dès maintenant.",
|
||||
"gradient": "linear-gradient(135deg, var(--accent-primary), var(--accent-secondary))",
|
||||
"since": "2025",
|
||||
"services": [
|
||||
"Sessions RP Red Dust Redemption 2"
|
||||
],
|
||||
"stats": [
|
||||
{
|
||||
"icon": "users",
|
||||
"value": "45+",
|
||||
"label": "Joueurs max"
|
||||
},
|
||||
{
|
||||
"icon": "home",
|
||||
"value": "+6",
|
||||
"label": "Paysages uniques"
|
||||
},
|
||||
{
|
||||
"icon": "play",
|
||||
"value": "+50",
|
||||
"label": "Sessions"
|
||||
}
|
||||
],
|
||||
"social": {
|
||||
"discord": "https://discord.gg/FUamNpf7hq",
|
||||
"twitter": "",
|
||||
"instagram": "",
|
||||
"youtube": "",
|
||||
"tiktok": "",
|
||||
"website": "#",
|
||||
"epicgames": "https://www.fortnite.com/@kayrix"
|
||||
},
|
||||
"logo": "assets/images/partners/red-dust-redemption.jpg"
|
||||
},
|
||||
{
|
||||
"name": "site-omega",
|
||||
"displayName": " Site Omega",
|
||||
"description": "Site dans l'univers SCP entièrement en UEFN.",
|
||||
"longDesc": "Site Omega est un projet ambitieux qui vise à recréer l'univers de la Fondation SCP dans Fortnite en utilisant UEFN. Les joueurs peuvent explorer des installations secrètes, interagir avec des anomalies et vivre des expériences uniques inspirées de l'univers SCP.",
|
||||
"gradient": "linear-gradient(135deg, #1a3a5e, #3a5e8b)",
|
||||
"since": "2025",
|
||||
"services": [
|
||||
"Map UEFN",
|
||||
"Bot Discord",
|
||||
"Branding"
|
||||
|
||||
],
|
||||
"stats": [
|
||||
{
|
||||
"icon": "star",
|
||||
"value": "?",
|
||||
"label": "Stat 1"
|
||||
},
|
||||
{
|
||||
"icon": "zap",
|
||||
"value": "?",
|
||||
"label": "Stat 2"
|
||||
},
|
||||
{
|
||||
"icon": "heart",
|
||||
"value": "?",
|
||||
"label": "Stat 3"
|
||||
}
|
||||
],
|
||||
"social": {
|
||||
"discord": "#",
|
||||
"twitter": "",
|
||||
"instagram": "",
|
||||
"youtube": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"tiktok": "",
|
||||
"website": "#",
|
||||
"epicgames": "https://www.fortnite.com/@omega_kube_team"
|
||||
},
|
||||
"logo": "assets/images/partners/site-omega.png"
|
||||
}
|
||||
]
|
||||
491
assets/data/projects.json
Normal file
|
|
@ -0,0 +1,491 @@
|
|||
[
|
||||
{
|
||||
"id": 1,
|
||||
"category": "bot",
|
||||
"title": "Kuby",
|
||||
"desc": "Un bot multifonction, de sécurité, d'administration et utilitaire.",
|
||||
"gradient": "linear-gradient(135deg, var(--accent-primary), var(--accent-secondary))",
|
||||
"year": "2025",
|
||||
"duration": "3 semaines",
|
||||
"features": [
|
||||
"Système d'absences staff",
|
||||
"Systèmes de sécurité et logs",
|
||||
"Gestion de sanctions",
|
||||
"Système de convocations",
|
||||
"Tickets avec stats",
|
||||
"Et bien plus à venir"
|
||||
],
|
||||
"tech": [
|
||||
{
|
||||
"name": "Python",
|
||||
"icon": "code"
|
||||
},
|
||||
{
|
||||
"name": "Discord.py",
|
||||
"icon": "message-circle"
|
||||
},
|
||||
{
|
||||
"name": "MySQL",
|
||||
"icon": "database"
|
||||
},
|
||||
{
|
||||
"name": "Bash",
|
||||
"icon": "terminal"
|
||||
}
|
||||
],
|
||||
"stats": [
|
||||
{
|
||||
"icon": "users",
|
||||
"value": "1075+",
|
||||
"label": "Utilisateurs"
|
||||
},
|
||||
{
|
||||
"icon": "terminal",
|
||||
"value": "150+",
|
||||
"label": "Commandes"
|
||||
},
|
||||
{
|
||||
"icon": "activity",
|
||||
"value": "97%",
|
||||
"label": "Uptime"
|
||||
},
|
||||
{
|
||||
"icon": "clock",
|
||||
"value": "3",
|
||||
"label": "Semaines de dev"
|
||||
}
|
||||
],
|
||||
"images": "assets/images/projects/kuby/",
|
||||
"files": [
|
||||
"imgk-1.png",
|
||||
"yt:dQw4w9WgXcQ"
|
||||
],
|
||||
"social": {
|
||||
"discord": "https://discord.gg/FwvNkvu5dy",
|
||||
"instagram": "",
|
||||
"tiktok": "",
|
||||
"youtube": "",
|
||||
"twitter": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"category": "map",
|
||||
"title": "Victoria's Vice",
|
||||
"desc": "Victoria's Vice est serveur RP whitelist Fortnite, Victoria's Vice, qui est basé sur un thème italien ! Plongez-vous dans un univers authentique où chaque partie est une immersion dans les rues, la culture, et l'ambiance unique italienne.",
|
||||
"gradient": "linear-gradient(135deg, #5E2040, #8b3a62)",
|
||||
"year": "2025",
|
||||
"duration": "4 mois",
|
||||
"features": [
|
||||
"économie réaliste",
|
||||
"30+ bâtiments",
|
||||
"Système jour/nuit",
|
||||
"Serveur Whitelist",
|
||||
"Métiers légaux variés",
|
||||
"Bot RP Vicy exclusif"
|
||||
],
|
||||
"tech": [
|
||||
{
|
||||
"name": "Fortnite Creative",
|
||||
"icon": "map"
|
||||
},
|
||||
{
|
||||
"name": "UEFN",
|
||||
"icon": "layers"
|
||||
},
|
||||
{
|
||||
"name": "Verse",
|
||||
"icon": "cpu"
|
||||
},
|
||||
{
|
||||
"name": "Texturing",
|
||||
"icon": "image"
|
||||
}
|
||||
],
|
||||
"stats": [
|
||||
{
|
||||
"icon": "play",
|
||||
"value": "30+",
|
||||
"label": "Sessions"
|
||||
},
|
||||
{
|
||||
"icon": "home",
|
||||
"value": "30+",
|
||||
"label": "Bâtiments"
|
||||
},
|
||||
{
|
||||
"icon": "users",
|
||||
"value": "70",
|
||||
"label": "Joueurs max"
|
||||
},
|
||||
{
|
||||
"icon": "calendar",
|
||||
"value": "4",
|
||||
"label": "Mois de dev"
|
||||
}
|
||||
],
|
||||
"images": "assets/images/projects/victorias-vice-1/",
|
||||
"files": [
|
||||
"imgvv1-1.jpg",
|
||||
"imgvv1-2.jpg",
|
||||
"imgvv1-3.jpg",
|
||||
"imgvv1-4.jpg",
|
||||
"imgvv1-5.jpg",
|
||||
"imgvv1-6.jpg",
|
||||
"imgvv1-7.jpg",
|
||||
"imgvv1-8.jpg",
|
||||
"TrailerOfficielVV1.mp4"
|
||||
],
|
||||
"social": {
|
||||
"discord": "https://discord.gg/8D564jm2SD",
|
||||
"instagram": "",
|
||||
"tiktok": "",
|
||||
"youtube": "",
|
||||
"twitter": "",
|
||||
"epicgames": "https://www.fortnite.com/@omega_kube_team"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"category": "bot",
|
||||
"title": "Vicy",
|
||||
"desc": "Bot RP ville avec économie réaliste, tickets whitelist automatisé et cartes d'identités.",
|
||||
"gradient": "linear-gradient(135deg, #3a1f5e, #8b3a62)",
|
||||
"year": "2024",
|
||||
"duration": "",
|
||||
"features": [
|
||||
"Économie réaliste",
|
||||
"Gestion ticket Whitelist",
|
||||
"Carte d'identités",
|
||||
"Prise de RDV",
|
||||
"système de comptes avec RIB",
|
||||
"Fonctionnalités bonus"
|
||||
],
|
||||
"tech": [
|
||||
{
|
||||
"name": "Python",
|
||||
"icon": "code"
|
||||
},
|
||||
{
|
||||
"name": "Discord.py",
|
||||
"icon": "message-circle"
|
||||
},
|
||||
{
|
||||
"name": "MySQL",
|
||||
"icon": "database"
|
||||
},
|
||||
{
|
||||
"name": "Pillow",
|
||||
"icon": "image"
|
||||
}
|
||||
],
|
||||
"stats": [
|
||||
{
|
||||
"icon": "users",
|
||||
"value": "300+",
|
||||
"label": "Utilisateurs"
|
||||
},
|
||||
{
|
||||
"icon": "terminal",
|
||||
"value": "100+",
|
||||
"label": "Commandes"
|
||||
},
|
||||
{
|
||||
"icon": "credit-card",
|
||||
"value": "60+",
|
||||
"label": "RIB générés"
|
||||
},
|
||||
{
|
||||
"icon": "activity",
|
||||
"value": "99%",
|
||||
"label": "Uptime"
|
||||
}
|
||||
],
|
||||
"images": "assets/images/projects/vicy/",
|
||||
"files": [
|
||||
"imgvicy-1.png"
|
||||
],
|
||||
"social": {
|
||||
"discord": "https://discord.gg/8D564jm2SD",
|
||||
"instagram": "",
|
||||
"tiktok": "",
|
||||
"youtube": "",
|
||||
"twitter": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"category": "graphisme",
|
||||
"title": "Branding Omega Kube",
|
||||
"desc": "Identité visuelle complète incluant logo, bannières, emojis personnalisés et templates pour Omega Kube.",
|
||||
"gradient": "linear-gradient(135deg, #2d1b5e, #5E2040)",
|
||||
"year": "2024",
|
||||
"duration": "1 semaine",
|
||||
"features": [
|
||||
"Logo principal",
|
||||
"Charte graphique",
|
||||
"Bannières",
|
||||
"Animations After Effects"
|
||||
],
|
||||
"tech": [
|
||||
{
|
||||
"name": "Photoshop",
|
||||
"icon": "edit"
|
||||
},
|
||||
{
|
||||
"name": "Illustrator",
|
||||
"icon": "pen-tool"
|
||||
},
|
||||
{
|
||||
"name": "After Effects",
|
||||
"icon": "film"
|
||||
},
|
||||
{
|
||||
"name": "Figma",
|
||||
"icon": "pen-tool"
|
||||
}
|
||||
],
|
||||
"stats": [
|
||||
{
|
||||
"icon": "layers",
|
||||
"value": "5+",
|
||||
"label": "Éléments"
|
||||
},
|
||||
{
|
||||
"icon": "clock",
|
||||
"value": "2h",
|
||||
"label": "After Effects"
|
||||
},
|
||||
{
|
||||
"icon": "pen-tool",
|
||||
"value": "3h",
|
||||
"label": "Illustrator"
|
||||
}
|
||||
],
|
||||
"images": "assets/images/projects/branding-omega-kube/",
|
||||
"files": [
|
||||
"imgk-1.png",
|
||||
"imgk-2.png",
|
||||
"videok-3.mp4"
|
||||
],
|
||||
"social": {
|
||||
"discord": "https://discord.gg/FwvNkvu5dy",
|
||||
"instagram": "",
|
||||
"tiktok": "",
|
||||
"youtube": "",
|
||||
"twitter": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"category": "map",
|
||||
"title": "Site Omega",
|
||||
"desc": "Site dans l'univers SCP entièrement en UEFN avec des systèmes innovants jamais vus dans le fortnite RP.",
|
||||
"gradient": "linear-gradient(135deg, #1a3a5e, #3a5e8b)",
|
||||
"year": "2026",
|
||||
"duration": "4 mois",
|
||||
"features": [
|
||||
"Intégration de SCP",
|
||||
"Cinématiques",
|
||||
"Modèles 3D",
|
||||
"Site Souterrain",
|
||||
"Terraforming extérieur"
|
||||
],
|
||||
"tech": [
|
||||
{
|
||||
"name": "Fortnite Creative",
|
||||
"icon": "map"
|
||||
},
|
||||
{
|
||||
"name": "UEFN",
|
||||
"icon": "layers"
|
||||
},
|
||||
{
|
||||
"name": "Verse",
|
||||
"icon": "cpu"
|
||||
},
|
||||
{
|
||||
"name": "Custom Props",
|
||||
"icon": "box"
|
||||
}
|
||||
],
|
||||
"stats": [
|
||||
{
|
||||
"icon": "grid",
|
||||
"value": "6+",
|
||||
"label": "Secteurs"
|
||||
},
|
||||
{
|
||||
"icon": "shield",
|
||||
"value": "15+",
|
||||
"label": "SCP"
|
||||
},
|
||||
{
|
||||
"icon": "cpu",
|
||||
"value": "30+",
|
||||
"label": "Systèmes"
|
||||
}
|
||||
],
|
||||
"images": "assets/images/projects/site-omega/",
|
||||
"files": [
|
||||
"imgso-1.png",
|
||||
"imgso-2.png",
|
||||
"imgso-3.png",
|
||||
"imgso-4.png",
|
||||
"imgso-5.png",
|
||||
"imgso-6.png",
|
||||
"imgso-7.png",
|
||||
"imgso-8.png",
|
||||
"imgso-9.jpg"
|
||||
],
|
||||
"social": {
|
||||
"discord": "",
|
||||
"instagram": "",
|
||||
"tiktok": "",
|
||||
"youtube": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"twitter": "",
|
||||
"epicgames": "https://www.fortnite.com/@omega_kube_team"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"category": "map",
|
||||
"title":"Victoria's Vice 2",
|
||||
"desc": "Suite de Victoria's Vice, avec une map plus grande, plus de bâtiments, plus de systèmes et une immersion encore plus poussée dans l'univers italien.",
|
||||
"gradient": "linear-gradient(135deg, #5E2040, #8b3a62)",
|
||||
"year": "2026",
|
||||
"duration": "en cours...",
|
||||
"features": [
|
||||
"économie réaliste",
|
||||
"50+ bâtiments",
|
||||
"Système jour/nuit",
|
||||
"Serveur Whitelist",
|
||||
"Métiers légaux variés",
|
||||
"Bot RP Vicy exclusif"
|
||||
],
|
||||
"tech": [
|
||||
{
|
||||
"name": "Fortnite Creative",
|
||||
"icon": "map"
|
||||
},
|
||||
{
|
||||
"name": "UEFN",
|
||||
"icon": "layers"
|
||||
},
|
||||
{
|
||||
"name": "Verse",
|
||||
"icon": "cpu"
|
||||
},
|
||||
{
|
||||
"name": "Texturing",
|
||||
"icon": "image"
|
||||
}
|
||||
],
|
||||
"stats": [
|
||||
{
|
||||
"icon": "play",
|
||||
"value": "en cours...",
|
||||
"label": "Sessions"
|
||||
},
|
||||
{
|
||||
"icon": "home",
|
||||
"value": "en cours...",
|
||||
"label": "Bâtiments"
|
||||
},
|
||||
{
|
||||
"icon": "users",
|
||||
"value": "en cours...",
|
||||
"label": "Joueurs max"
|
||||
},
|
||||
{
|
||||
"icon": "calendar",
|
||||
"value": "en cours...",
|
||||
"label": "Mois de dev"
|
||||
}
|
||||
],
|
||||
"images": "assets/images/projects/victorias-vice-2/",
|
||||
"files": [
|
||||
"imgvv2-1.jpg",
|
||||
"imgvv2-2.jpg",
|
||||
"imgvv2-3.jpg",
|
||||
"https://www.youtube.com/watch?v=Lx-1FWOJN9E"
|
||||
],
|
||||
"social": {
|
||||
"discord": "https://discord.gg/AM3e58cZvX",
|
||||
"instagram": "",
|
||||
"tiktok": "",
|
||||
"youtube": "",
|
||||
"twitter": "",
|
||||
"epicgames": "https://www.fortnite.com/@omega_kube_team"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"category": "bot",
|
||||
"title": "Beta",
|
||||
"desc": "En cours de développement chez Omega Kube.",
|
||||
"gradient": "linear-gradient(135deg, #000000, #434343)",
|
||||
"year": "2026",
|
||||
"duration": "en cours...",
|
||||
"features": [
|
||||
"en cours..."
|
||||
],
|
||||
"tech": [
|
||||
{
|
||||
"name": "Python",
|
||||
"icon": "code"
|
||||
},
|
||||
{
|
||||
"name": "Discord.py",
|
||||
"icon": "message-circle"
|
||||
},
|
||||
{
|
||||
"name": "MySQL",
|
||||
"icon": "database"
|
||||
},
|
||||
{
|
||||
"name": "Pillow",
|
||||
"icon": "image"
|
||||
},
|
||||
{
|
||||
"name": "Autre tech",
|
||||
"icon": "tool"
|
||||
}
|
||||
],
|
||||
"stats": [
|
||||
{
|
||||
"icon": "users",
|
||||
"value": "en cours...",
|
||||
"label": "Utilisateurs"
|
||||
},
|
||||
{
|
||||
"icon": "terminal",
|
||||
"value": "en cours...",
|
||||
"label": "Commandes"
|
||||
},
|
||||
{
|
||||
"icon": "credit-card",
|
||||
"value": "en cours...",
|
||||
"label": "RP menés"
|
||||
},
|
||||
{
|
||||
"icon": "activity",
|
||||
"value": "en cours...",
|
||||
"label": "Uptime"
|
||||
}
|
||||
],
|
||||
"images": "assets/images/projects/beta/",
|
||||
"files": [
|
||||
"imgb-1.png",
|
||||
"imgb-2.png",
|
||||
"yt:dQw4w9WgXcQ"
|
||||
],
|
||||
"social": {
|
||||
"discord": "https://discord.gg/FwvNkvu5dy",
|
||||
"instagram": "",
|
||||
"tiktok": "",
|
||||
"youtube": "",
|
||||
"twitter": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
174
assets/data/services.json
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
[
|
||||
{
|
||||
"id": "bots",
|
||||
"nav": {
|
||||
"label": "Bots Discord",
|
||||
"icon": "code",
|
||||
"stamp": {"type": "popular","text": "Populaire"}
|
||||
},
|
||||
"visual": {
|
||||
"accentFrom": "#8b3a62",
|
||||
"accentTo": "#a64d79",
|
||||
"stamp": {"type": "popular","text": "★ Populaire","animated": true}
|
||||
},
|
||||
"title": "Bots Discord RP",
|
||||
"description": "Des bots entièrement personnalisés pour animer et gérer vos serveurs Role Play. Nous développons des systèmes complets adaptés à votre univers et à vos mécaniques de jeu.",
|
||||
"highlights": [
|
||||
{"value": "2-4","label": "Semaines de dev"},
|
||||
{"value": "1 mois","label": "Support inclus"},
|
||||
{"value": "100%", "label": "Personnalisable"}
|
||||
],
|
||||
"features": [
|
||||
{"icon": "dollar-sign", "title": "Économie virtuelle","desc": "Monnaie, banque, transactions"},
|
||||
{"icon": "briefcase","title": "Système de métiers","desc": "Jobs, skills, progression"},
|
||||
{"icon": "shield","title": "Combat & Stats","desc": "PvP, PvE, attributs"},
|
||||
{"icon": "box","title": "Inventaire avancé","desc": "Items, crafting, échanges"},
|
||||
{"icon": "flag","title": "Factions & Guildes","desc": "Clans, territoires, guerres"},
|
||||
{"icon": "settings", "title": "Panel Admin","desc": "Interface web de gestion"}
|
||||
],
|
||||
"cta": [
|
||||
{"label": "Demander un devis","href": "../index.html#contact", "style": "primary","icon": "message-circle"},
|
||||
{"label": "Voir nos réalisations","href": "realisations.html","style": "secondary","icon": "eye"}
|
||||
],
|
||||
"note": []
|
||||
},
|
||||
{
|
||||
"id": "maps",
|
||||
"nav": {
|
||||
"label": "Maps RP",
|
||||
"icon": "map",
|
||||
"stamp": {"type": "soon","text": "Bientôt dispo"}
|
||||
},
|
||||
"visual": {
|
||||
"accentFrom": "#1a5e8b",
|
||||
"accentTo": "#3a8bb5",
|
||||
"stamp": { "type": "soon","text": "✦ Bientôt dispo","animated": true }
|
||||
},
|
||||
"title": "Maps RP",
|
||||
"description": "Des environnements immersifs créés sur mesure pour vos sessions Role Play. De la ville moderne au village médiéval, nous donnons vie à tous les univers.",
|
||||
"highlights": [
|
||||
{"value": "3-6","label": "Semaines de création"},
|
||||
{"value": "50+","label": "Joueurs supportés"},
|
||||
{"value": "UEFN","label": "Compatible"}
|
||||
],
|
||||
"features": [
|
||||
{"icon": "home","title": "Bâtiments détaillés","desc": "Intérieurs accessibles et déco"},
|
||||
{"icon": "sun","title": "Cycle jour/nuit","desc": "Ambiance dynamique"},
|
||||
{"icon": "zap","title": "Zones interactives","desc": "Scripts et triggers"},
|
||||
{"icon": "cpu","title": "Optimisation","desc": "Performance garantie"},
|
||||
{"icon": "map-pin","title": "Points d'intérêt","desc": "Lieux uniques et mémorables"},
|
||||
{"icon": "edit-3","title": "Modifications","desc": "Ajustements post-livraison"}
|
||||
],
|
||||
"cta": [
|
||||
{"label": "Demander un devis","href": "../index.html#contact","style": "primary","icon": "message-circle"},
|
||||
{"label": "Voir nos maps","href": "realisations.html","style": "secondary","icon": "eye"}
|
||||
],
|
||||
"note": []
|
||||
},
|
||||
{
|
||||
"id": "graphisme",
|
||||
"nav": {
|
||||
"label": "Graphisme",
|
||||
"icon": "image",
|
||||
"stamp": null
|
||||
},
|
||||
"visual": {
|
||||
"accentFrom": "#8b5e3a",
|
||||
"accentTo": "#b58a5a",
|
||||
"stamp": null
|
||||
},
|
||||
"title": "Créations Graphiques",
|
||||
"description": "Une identité visuelle forte et cohérente pour vos projets. Du logo aux bannières, nous créons des visuels qui marquent les esprits et attirent votre communauté.",
|
||||
"highlights": [
|
||||
{"value": "1-2","label": "Semaines"},
|
||||
{"value": "∞","label": "Révisions*"},
|
||||
{"value": "HD","label": "Qualité"}
|
||||
],
|
||||
"features": [
|
||||
{"icon": "hexagon","title": "Logo & Branding","desc": "Identité visuelle complète"},
|
||||
{"icon": "layout","title": "Bannières Discord","desc": "Server banner, splash, icône"},
|
||||
{"icon": "smile","title": "Emojis custom","desc": "Pack emojis personnalisés"},
|
||||
{"icon": "grid","title": "Icônes & Assets","desc": "Items, skills, badges"},
|
||||
{"icon": "play-circle","title": "Animations","desc": "GIFs et motion design"},
|
||||
{"icon": "book-open","title": "Guide de style","desc": "Charte graphique fournie"}
|
||||
],
|
||||
"cta": [
|
||||
{"label": "Demander un devis","href": "../index.html#contact","style": "primary","icon": "message-circle" },
|
||||
{"label": "Voir nos créations","href": "realisations.html","style": "secondary","icon": "eye"}
|
||||
],
|
||||
"note": [
|
||||
{"value": "*","label": "Révisions illimitées sous conditions, voir FAQ"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "consulting",
|
||||
"nav": {
|
||||
"label": "Consulting",
|
||||
"icon": "users",
|
||||
"stamp": {"type": "free","text": "Gratuit"}
|
||||
},
|
||||
"visual": {
|
||||
"accentFrom": "#3a8b5e",
|
||||
"accentTo": "#5ab57a",
|
||||
"stamp": {"type": "free","text": "✓ Gratuit","animated": true}
|
||||
},
|
||||
"title": "Consulting & Suivi",
|
||||
"titleStamp": {"type": "free","text": "Gratuit"},
|
||||
"description": "Bénéficiez de notre expérience pour structurer, lancer et développer votre projet RP. Conseils stratégiques et accompagnement personnalisé.",
|
||||
"highlights": [
|
||||
{"value": "50+","label": "Projets accompagnés" },
|
||||
{"value": "3 ans","label": "D'expérience"},
|
||||
{"value": "24/7","label": "Support Discord"}
|
||||
],
|
||||
"features": [
|
||||
{"icon": "search","title": "Audit serveur","desc": "Analyse de l'existant"},
|
||||
{"icon": "trending-up","title": "Stratégie croissance","desc": "Plan de développement"},
|
||||
{"icon": "user-check","title": "Formation staff","desc": "Équipe modération"},
|
||||
{"icon": "heart","title": "Expérience joueur","desc": "Optimisation UX"},
|
||||
{"icon": "calendar","title": "Planning events","desc": "Calendrier d'animations"},
|
||||
{"icon": "dollar-sign","title": "Monétisation","desc": "Conseils revenus"}
|
||||
],
|
||||
"cta": [
|
||||
{"label": "Prendre rendez-vous","href": "../index.html#contact","style": "primary","icon": "message-circle"}
|
||||
],
|
||||
"note": [
|
||||
{"value": "✓","label": "Première consultation gratuite et sans engagement"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "pack",
|
||||
"nav": {
|
||||
"label": "Pack Complet",
|
||||
"icon": "package",
|
||||
"stamp": {"type": "best","text": "Meilleur choix"}
|
||||
},
|
||||
"visual": {
|
||||
"accentFrom": "#8b3a8b",
|
||||
"accentTo": "#b55ab5",
|
||||
"stamp": {"type": "best","text": "✓ Meilleur choix","animated": true}
|
||||
},
|
||||
"title": "Pack Serveur Complet",
|
||||
"titleStamp": {"type": "promo","text": "-20%"},
|
||||
"description": "La solution tout-en-un pour lancer votre projet RP. Bot personnalisé, map Fortnite, identité visuelle et accompagnement : tout est inclus pour un lancement réussi.",
|
||||
"highlights": [
|
||||
{"value": "-20%","label": "VS services séparés*"},
|
||||
{"value": "3 mois","label": "Support inclus"},
|
||||
{"value": "Clé en main","label": "Prêt à lancer"}
|
||||
],
|
||||
"features": [
|
||||
{"icon": "code","title": "Bot Discord RP","desc": "Système complet personnalisé"},
|
||||
{"icon": "map","title": "Map RP","desc": "Environnement sur mesure"},
|
||||
{"icon": "image","title": "Pack graphique","desc": "Logo, bannières, emojis"},
|
||||
{"icon": "server","title": "Config Discord","desc": "Serveur prêt à l'emploi"},
|
||||
{"icon": "book","title": "Documentation","desc": "Guides et tutoriels"},
|
||||
{"icon": "headphones","title": "Support 3 mois","desc": "Accompagnement lancement"}
|
||||
],
|
||||
"cta": [
|
||||
{"label": "Demander un devis pack","href": "../index.html#contact","style": "primary","icon": "message-circle"},
|
||||
{"label": "Voir nos packs livrés","href": "realisations.html","style": "secondary","icon": "eye"}
|
||||
],
|
||||
"note": [
|
||||
{"value": "*","label": "Réduction de 20% appliquée automatiquement vs services séparés"}
|
||||
]
|
||||
}
|
||||
]
|
||||
264
assets/data/team.json
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
[
|
||||
{
|
||||
"id": "534003230435442690",
|
||||
"name": "Nixfix",
|
||||
"role": "Directeur",
|
||||
"avatar": "N",
|
||||
"bio": "Président de l'association et coordinateur des équipes d'Omega Kube, Nixfix donne vie aux identités visuelles des projets RP. Passionné de design graphique, il excelle aussi bien dans la création de logos que dans la gestion de projets. Son style unique allie une esthétique gaming affirmée à un véritable professionnalisme, afin de proposer des visuels percutants et cohérents.",
|
||||
"stats": [
|
||||
{
|
||||
"value": "6",
|
||||
"label": "Ans d'exp."
|
||||
},
|
||||
{
|
||||
"value": "15+",
|
||||
"label": "Projets"
|
||||
},
|
||||
{
|
||||
"value": "★★★",
|
||||
"label": "Expert"
|
||||
}
|
||||
],
|
||||
"skills": [
|
||||
"Polyvalent",
|
||||
"Gestion de projets",
|
||||
"Illustrator",
|
||||
"After Effects",
|
||||
"Figma",
|
||||
"UEFN",
|
||||
"Photoshop",
|
||||
"Verse"
|
||||
],
|
||||
"social": {
|
||||
"discord": "",
|
||||
"twitter": "",
|
||||
"instagram": "",
|
||||
"youtube": "",
|
||||
"tiktok": "",
|
||||
"github": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "353942721997832195",
|
||||
"name": "MG",
|
||||
"role": "Co-Directeur",
|
||||
"avatar": "M",
|
||||
"bio": "Je suis un développeur UEFN et Builder Minecraft polyvalent avec une solide expérience dans la création de systèmes pour divers Serveurs RP. J'ai travaillé sur des projets de plus ou moins grande envergure sur diverses plateformes de jeux. J'ai aussi une certaine expérience en gestion de projet et je me montre polyvalent pour beaucoup de tâches",
|
||||
"stats": [
|
||||
{
|
||||
"value": "6",
|
||||
"label": "Ans d'exp."
|
||||
},
|
||||
{
|
||||
"value": "25+",
|
||||
"label": "Systèmes"
|
||||
},
|
||||
{
|
||||
"value": "∞",
|
||||
"label": "Fan de Louis de Funès"
|
||||
}
|
||||
],
|
||||
"skills": [
|
||||
"Polyvalent",
|
||||
"Gestion de projets",
|
||||
"UEFN",
|
||||
"Builder Minecraft"
|
||||
],
|
||||
"social": {
|
||||
"discord": "",
|
||||
"twitter": "",
|
||||
"instagram": "",
|
||||
"youtube": "",
|
||||
"tiktok": "",
|
||||
"github": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "971446412690722826",
|
||||
"name": "Gameur",
|
||||
"role": "Agent - Développeur Principal",
|
||||
"avatar": "G",
|
||||
"bio": "Avec plus de 3 ans d'expérience dans le self-hosting, j'ai créé des bots pour des communautés de toutes tailles, allant de petits serveurs à des projets d'envergure. Mon expertise couvre la création de commandes personnalisées, l'intégration d'API tierces et la gestion de bases de données pour offrir des fonctionnalités avancées et une expérience utilisateur fluide.",
|
||||
"stats": [
|
||||
{
|
||||
"value": "3",
|
||||
"label": "Ans d'exp."
|
||||
},
|
||||
{
|
||||
"value": "50+",
|
||||
"label": "Projets"
|
||||
},
|
||||
{
|
||||
"value": "★★★★",
|
||||
"label": "Intermédiaire - Avancé"
|
||||
}
|
||||
],
|
||||
"skills": [
|
||||
"Python",
|
||||
"Discord.py",
|
||||
"API REST",
|
||||
"Docker",
|
||||
"self-hosting",
|
||||
"Linux",
|
||||
"Gestion de projets"
|
||||
],
|
||||
"social": {
|
||||
"discord": "",
|
||||
"twitter": "",
|
||||
"instagram": "",
|
||||
"youtube": "",
|
||||
"tiktok": "",
|
||||
"github": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "561485865562406914",
|
||||
"name": "KIRITO_KMA",
|
||||
"role": "Agent - Développeur Full-Stack",
|
||||
"avatar": "K",
|
||||
"bio": "Développeur Full-Stack passionné, maîtrisent une large gamme de technologies pour créer des solutions complètes et performantes. Mon expertise s'étend du développement backend robuste à la création d'interfaces utilisateur intuitives, en passant par la gestion de bases de données et l'intégration d'API tierces. Mon panel de compétences s'étend également à la mise en place d'environnements de développement et de déploiement efficaces, garantissant ainsi la qualité et la fiabilité de chaque projet sur lequel je travaille.",
|
||||
"stats": [
|
||||
{
|
||||
"value": "4",
|
||||
"label": "Ans d'exp."
|
||||
},
|
||||
{
|
||||
"value": "50+",
|
||||
"label": "Projets menés"
|
||||
},
|
||||
{
|
||||
"value": "1000%",
|
||||
"label": "Maîtrise du shifoumi"
|
||||
}
|
||||
],
|
||||
"skills": [
|
||||
"Dev Full-Stack",
|
||||
"SEO",
|
||||
"Map-Maker",
|
||||
"Bot Discord",
|
||||
"Scénariste",
|
||||
"Shifoumi",
|
||||
"Command Blocks Minecraft"
|
||||
],
|
||||
"social": {
|
||||
"discord": "",
|
||||
"twitter": "",
|
||||
"instagram": "",
|
||||
"youtube": "",
|
||||
"tiktok": "",
|
||||
"github": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "1324439906046574693",
|
||||
"name": "Lowei",
|
||||
"role": "Agent - Développeur Backend",
|
||||
"avatar": "L",
|
||||
"bio": "Salut ! Je suis un développeur backend passionné par la création de systèmes robustes, performants et dans l'automatisation de tâches. N'hésite pas à checker mes réseaux ci-dessous !",
|
||||
"stats": [
|
||||
{
|
||||
"value": "5",
|
||||
"label": "Ans d'exp."
|
||||
},
|
||||
{
|
||||
"value": "20+",
|
||||
"label": "Systèmes"
|
||||
},
|
||||
{
|
||||
"value": "∞",
|
||||
"label": "Disponibilité"
|
||||
}
|
||||
],
|
||||
"skills": [
|
||||
"Python",
|
||||
"HTML",
|
||||
"CSS",
|
||||
"C, C++",
|
||||
"Java",
|
||||
"Blender"
|
||||
],
|
||||
"social": {
|
||||
"discord": "",
|
||||
"twitter": "",
|
||||
"instagram": "",
|
||||
"youtube": "",
|
||||
"tiktok": "",
|
||||
"github": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "899299051080663050",
|
||||
"name": "Ryou",
|
||||
"role": "Agent - Modélisation 3D",
|
||||
"avatar": "R",
|
||||
"bio": "Hey, je suis Ryou, créatif et développeur passionné par la création sous toutes ses formes : code, 3D, concept art ou musique/sound design. J'aime construire des applications esthétiquement belles et utiles, ou des jeux harmonieux et amusants. Je conçois également des modèles 3D pour VTubers, animations ou jeux vidéo. Mes commissions sont ouvertes, n'hésitez pas à me follow sur mes réseaux !",
|
||||
"stats": [
|
||||
{
|
||||
"value": "6",
|
||||
"label": "Ans d'exp."
|
||||
},
|
||||
{
|
||||
"value": "5+",
|
||||
"label": "Projets"
|
||||
},
|
||||
{
|
||||
"value": "15+",
|
||||
"label": "Commissions réalisées"
|
||||
}
|
||||
],
|
||||
"skills": [
|
||||
"Blender",
|
||||
"HTML",
|
||||
"TypeScript",
|
||||
"JavaScript",
|
||||
"C, C#, C++",
|
||||
"Fl Studio",
|
||||
"Piano, Guitare, Bass"
|
||||
],
|
||||
"social": {
|
||||
"discord": "",
|
||||
"twitter": "https://x.com/ryoucafe_1",
|
||||
"instagram": "https://www.instagram.com/ryoucafe/",
|
||||
"youtube": "",
|
||||
"tiktok": "",
|
||||
"github": "https://github.com/ryoucafe"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "1151495567256584253",
|
||||
"name": "Kuby",
|
||||
"role": "Bot Discord",
|
||||
"avatar": "K",
|
||||
"bio": "Kuby est le gardien de la qualité du moindre octets au quetta-octets chez OmégaKube. Aucune commande ne lui échappe. Il execute méticuleusement chaque instructions, chaque commande, chaque interaction pour garantir une expérience sans accroc aux utilisateurs finaux.",
|
||||
"stats": [
|
||||
{
|
||||
"value": "2",
|
||||
"label": "Yotta d'exp sur Discord"
|
||||
},
|
||||
{
|
||||
"value": "500+",
|
||||
"label": "Commandes réalisées"
|
||||
},
|
||||
{
|
||||
"value": "0",
|
||||
"label": "Vulnérabilités"
|
||||
}
|
||||
],
|
||||
"skills": [
|
||||
"Moderation",
|
||||
"Events",
|
||||
"Documentation",
|
||||
"Serveur Discord",
|
||||
"Administration",
|
||||
"Bot"
|
||||
],
|
||||
"social": {
|
||||
"discord": "",
|
||||
"twitter": "",
|
||||
"instagram": "",
|
||||
"youtube": "",
|
||||
"tiktok": "",
|
||||
"github": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
BIN
assets/fonts/Montserrat/Montserrat-Italic-VariableFont_wght.ttf
Normal file
BIN
assets/fonts/Montserrat/Montserrat-VariableFont_wght.ttf
Normal file
93
assets/fonts/Montserrat/OFL.txt
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
Copyright 2024 The Montserrat.Git Project Authors (https://github.com/JulietaUla/Montserrat.git)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
81
assets/fonts/Montserrat/README.txt
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
Montserrat Variable Font
|
||||
========================
|
||||
|
||||
This download contains Montserrat as both variable fonts and static fonts.
|
||||
|
||||
Montserrat is a variable font with this axis:
|
||||
wght
|
||||
|
||||
This means all the styles are contained in these files:
|
||||
Montserrat-VariableFont_wght.ttf
|
||||
Montserrat-Italic-VariableFont_wght.ttf
|
||||
|
||||
If your app fully supports variable fonts, you can now pick intermediate styles
|
||||
that aren’t available as static fonts. Not all apps support variable fonts, and
|
||||
in those cases you can use the static font files for Montserrat:
|
||||
static/Montserrat-Thin.ttf
|
||||
static/Montserrat-ExtraLight.ttf
|
||||
static/Montserrat-Light.ttf
|
||||
static/Montserrat-Regular.ttf
|
||||
static/Montserrat-Medium.ttf
|
||||
static/Montserrat-SemiBold.ttf
|
||||
static/Montserrat-Bold.ttf
|
||||
static/Montserrat-ExtraBold.ttf
|
||||
static/Montserrat-Black.ttf
|
||||
static/Montserrat-ThinItalic.ttf
|
||||
static/Montserrat-ExtraLightItalic.ttf
|
||||
static/Montserrat-LightItalic.ttf
|
||||
static/Montserrat-Italic.ttf
|
||||
static/Montserrat-MediumItalic.ttf
|
||||
static/Montserrat-SemiBoldItalic.ttf
|
||||
static/Montserrat-BoldItalic.ttf
|
||||
static/Montserrat-ExtraBoldItalic.ttf
|
||||
static/Montserrat-BlackItalic.ttf
|
||||
|
||||
Get started
|
||||
-----------
|
||||
|
||||
1. Install the font files you want to use
|
||||
|
||||
2. Use your app's font picker to view the font family and all the
|
||||
available styles
|
||||
|
||||
Learn more about variable fonts
|
||||
-------------------------------
|
||||
|
||||
https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
|
||||
https://variablefonts.typenetwork.com
|
||||
https://medium.com/variable-fonts
|
||||
|
||||
In desktop apps
|
||||
|
||||
https://theblog.adobe.com/can-variable-fonts-illustrator-cc
|
||||
https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
|
||||
|
||||
Online
|
||||
|
||||
https://developers.google.com/fonts/docs/getting_started
|
||||
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
|
||||
https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
|
||||
|
||||
Installing fonts
|
||||
|
||||
MacOS: https://support.apple.com/en-us/HT201749
|
||||
Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
|
||||
Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
|
||||
|
||||
Android Apps
|
||||
|
||||
https://developers.google.com/fonts/docs/android
|
||||
https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts
|
||||
|
||||
License
|
||||
-------
|
||||
Please read the full license text (OFL.txt) to understand the permissions,
|
||||
restrictions and requirements for usage, redistribution, and modification.
|
||||
|
||||
You can use them in your products & projects – print or digital,
|
||||
commercial or otherwise.
|
||||
|
||||
This isn't legal advice, please consider consulting a lawyer and see the full
|
||||
license for all details.
|
||||
BIN
assets/fonts/Montserrat/static/Montserrat-Black.ttf
Normal file
BIN
assets/fonts/Montserrat/static/Montserrat-BlackItalic.ttf
Normal file
BIN
assets/fonts/Montserrat/static/Montserrat-Bold.ttf
Normal file
BIN
assets/fonts/Montserrat/static/Montserrat-BoldItalic.ttf
Normal file
BIN
assets/fonts/Montserrat/static/Montserrat-ExtraBold.ttf
Normal file
BIN
assets/fonts/Montserrat/static/Montserrat-ExtraBoldItalic.ttf
Normal file
BIN
assets/fonts/Montserrat/static/Montserrat-ExtraLight.ttf
Normal file
BIN
assets/fonts/Montserrat/static/Montserrat-ExtraLightItalic.ttf
Normal file
BIN
assets/fonts/Montserrat/static/Montserrat-Italic.ttf
Normal file
BIN
assets/fonts/Montserrat/static/Montserrat-Light.ttf
Normal file
BIN
assets/fonts/Montserrat/static/Montserrat-LightItalic.ttf
Normal file
BIN
assets/fonts/Montserrat/static/Montserrat-Medium.ttf
Normal file
BIN
assets/fonts/Montserrat/static/Montserrat-MediumItalic.ttf
Normal file
BIN
assets/fonts/Montserrat/static/Montserrat-Regular.ttf
Normal file
BIN
assets/fonts/Montserrat/static/Montserrat-SemiBold.ttf
Normal file
BIN
assets/fonts/Montserrat/static/Montserrat-SemiBoldItalic.ttf
Normal file
BIN
assets/fonts/Montserrat/static/Montserrat-Thin.ttf
Normal file
BIN
assets/fonts/Montserrat/static/Montserrat-ThinItalic.ttf
Normal file
BIN
assets/fonts/custom_font_large.otf
Normal file
BIN
assets/fonts/custom_font_small.otf
Normal file
BIN
assets/images/favicon/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
assets/images/favicon/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
assets/images/favicon/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/images/favicon/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 632 B |
BIN
assets/images/favicon/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/images/favicon/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
13
assets/images/favicon/site.webmanifest
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "Omega Kube",
|
||||
"short_name": "Omega Kube",
|
||||
"description": "L'univers du Role Play réinventé",
|
||||
"icons": [
|
||||
{ "src": "/assets/images/favicon/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
|
||||
{ "src": "/assets/images/favicon/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
|
||||
],
|
||||
"theme_color": "#0a0a0f",
|
||||
"background_color": "#0a0a0f",
|
||||
"display": "standalone",
|
||||
"start_url": "/"
|
||||
}
|
||||
BIN
assets/images/imgrd-1.jpg
Normal file
|
After Width: | Height: | Size: 389 KiB |
BIN
assets/images/imgrd-2.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
assets/images/logo.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
assets/images/old-logo.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
assets/images/partners/red-dust-redemption.jpg
Normal file
|
After Width: | Height: | Size: 389 KiB |
BIN
assets/images/partners/rickroll.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
assets/images/partners/site-omega.png
Normal file
|
After Width: | Height: | Size: 921 KiB |
BIN
assets/images/partners/the-last-refuge.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
assets/images/partners/victorias-vice.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
assets/images/partners/victorias-vicebis.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/images/projects/beta/imgb-1.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
assets/images/projects/beta/imgb-2.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
assets/images/projects/branding-omega-kube/gifk-4.gif
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
assets/images/projects/branding-omega-kube/imgk-1.png
Normal file
|
After Width: | Height: | Size: 208 KiB |
BIN
assets/images/projects/branding-omega-kube/imgk-2.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
assets/images/projects/branding-omega-kube/videok-3.mp4
Normal file
BIN
assets/images/projects/kuby/imgk-1.png
Normal file
|
After Width: | Height: | Size: 9 KiB |
BIN
assets/images/projects/kuby/imgk-1bis.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
assets/images/projects/site-omega/imgso-1.png
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
assets/images/projects/site-omega/imgso-2.png
Normal file
|
After Width: | Height: | Size: 3.9 MiB |
BIN
assets/images/projects/site-omega/imgso-3.png
Normal file
|
After Width: | Height: | Size: 4.4 MiB |
BIN
assets/images/projects/site-omega/imgso-4.png
Normal file
|
After Width: | Height: | Size: 3.9 MiB |
BIN
assets/images/projects/site-omega/imgso-5.png
Normal file
|
After Width: | Height: | Size: 4.7 MiB |
BIN
assets/images/projects/site-omega/imgso-6.png
Normal file
|
After Width: | Height: | Size: 3.7 MiB |
BIN
assets/images/projects/site-omega/imgso-7.png
Normal file
|
After Width: | Height: | Size: 3.5 MiB |
BIN
assets/images/projects/site-omega/imgso-8.png
Normal file
|
After Width: | Height: | Size: 4.2 MiB |
BIN
assets/images/projects/site-omega/imgso-9.jpg
Normal file
|
After Width: | Height: | Size: 437 KiB |
BIN
assets/images/projects/victorias-vice-1/TrailerOfficielVV1.mp4
Normal file
BIN
assets/images/projects/victorias-vice-1/imgvv1-1.jpg
Normal file
|
After Width: | Height: | Size: 195 KiB |
BIN
assets/images/projects/victorias-vice-1/imgvv1-2.jpg
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
assets/images/projects/victorias-vice-1/imgvv1-3.jpg
Normal file
|
After Width: | Height: | Size: 245 KiB |
BIN
assets/images/projects/victorias-vice-1/imgvv1-4.jpg
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
assets/images/projects/victorias-vice-1/imgvv1-5.jpg
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
assets/images/projects/victorias-vice-1/imgvv1-6.jpg
Normal file
|
After Width: | Height: | Size: 302 KiB |
BIN
assets/images/projects/victorias-vice-1/imgvv1-7.jpg
Normal file
|
After Width: | Height: | Size: 245 KiB |
BIN
assets/images/projects/victorias-vice-1/imgvv1-8.jpg
Normal file
|
After Width: | Height: | Size: 600 KiB |
BIN
assets/images/projects/victorias-vice-2/imgvv2-1.jpg
Normal file
|
After Width: | Height: | Size: 352 KiB |
BIN
assets/images/projects/victorias-vice-2/imgvv2-2.jpg
Normal file
|
After Width: | Height: | Size: 478 KiB |
BIN
assets/images/projects/victorias-vice-2/imgvv2-3.jpg
Normal file
|
After Width: | Height: | Size: 720 KiB |
BIN
assets/images/projects/vicy/imgvicy-1.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
assets/images/social/discord1.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
assets/images/social/discord2.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
assets/images/social/discord3.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
1
assets/images/social/epicgames.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg role="img" viewBox="0 0 24 24" width="20" height="20" xmlns="http://www.w3.org/2000/svg"><title>Epic Games</title><path fill="white" d="M3.537 0C2.165 0 1.66.506 1.66 1.879V18.44a4.262 4.262 0 00.02.433c.031.3.037.59.316.92.027.033.311.245.311.245.153.075.258.13.43.2l8.335 3.491c.433.199.614.276.928.27h.002c.314.006.495-.071.928-.27l8.335-3.492c.172-.07.277-.124.43-.2 0 0 .284-.211.311-.243.28-.33.285-.621.316-.92a4.261 4.261 0 00.02-.434V1.879c0-1.373-.506-1.88-1.878-1.88zm13.366 3.11h.68c1.138 0 1.688.553 1.688 1.696v1.88h-1.374v-1.8c0-.369-.17-.54-.523-.54h-.235c-.367 0-.537.17-.537.539v5.81c0 .369.17.54.537.54h.262c.353 0 .523-.171.523-.54V8.619h1.373v2.143c0 1.144-.562 1.71-1.7 1.71h-.694c-1.138 0-1.7-.566-1.7-1.71V4.82c0-1.144.562-1.709 1.7-1.709zm-12.186.08h3.114v1.274H6.117v2.603h1.648v1.275H6.117v2.774h1.74v1.275h-3.14zm3.816 0h2.198c1.138 0 1.7.564 1.7 1.708v2.445c0 1.144-.562 1.71-1.7 1.71h-.799v3.338h-1.4zm4.53 0h1.4v9.201h-1.4zm-3.13 1.235v3.392h.575c.354 0 .523-.171.523-.54V4.965c0-.368-.17-.54-.523-.54zm-3.74 10.147a1.708 1.708 0 01.591.108 1.745 1.745 0 01.49.299l-.452.546a1.247 1.247 0 00-.308-.195.91.91 0 00-.363-.068.658.658 0 00-.28.06.703.703 0 00-.224.163.783.783 0 00-.151.243.799.799 0 00-.056.299v.008a.852.852 0 00.056.31.7.7 0 00.157.245.736.736 0 00.238.16.774.774 0 00.303.058.79.79 0 00.445-.116v-.339h-.548v-.565H7.37v1.255a2.019 2.019 0 01-.524.307 1.789 1.789 0 01-.683.123 1.642 1.642 0 01-.602-.107 1.46 1.46 0 01-.478-.3 1.371 1.371 0 01-.318-.455 1.438 1.438 0 01-.115-.58v-.008a1.426 1.426 0 01.113-.57 1.449 1.449 0 01.312-.46 1.418 1.418 0 01.474-.309 1.58 1.58 0 01.598-.111 1.708 1.708 0 01.045 0zm11.963.008a2.006 2.006 0 01.612.094 1.61 1.61 0 01.507.277l-.386.546a1.562 1.562 0 00-.39-.205 1.178 1.178 0 00-.388-.07.347.347 0 00-.208.052.154.154 0 00-.07.127v.008a.158.158 0 00.022.084.198.198 0 00.076.066.831.831 0 00.147.06c.062.02.14.04.236.061a3.389 3.389 0 01.43.122 1.292 1.292 0 01.328.17.678.678 0 01.207.24.739.739 0 01.071.337v.008a.865.865 0 01-.081.382.82.82 0 01-.229.285 1.032 1.032 0 01-.353.18 1.606 1.606 0 01-.46.061 2.16 2.16 0 01-.71-.116 1.718 1.718 0 01-.593-.346l.43-.514c.277.223.578.335.9.335a.457.457 0 00.236-.05.157.157 0 00.082-.142v-.008a.15.15 0 00-.02-.077.204.204 0 00-.073-.066.753.753 0 00-.143-.062 2.45 2.45 0 00-.233-.062 5.036 5.036 0 01-.413-.113 1.26 1.26 0 01-.331-.16.72.72 0 01-.222-.243.73.73 0 01-.082-.36v-.008a.863.863 0 01.074-.359.794.794 0 01.214-.283 1.007 1.007 0 01.34-.185 1.423 1.423 0 01.448-.066 2.006 2.006 0 01.025 0zm-9.358.025h.742l1.183 2.81h-.825l-.203-.499H8.623l-.198.498h-.81zm2.197.02h.814l.663 1.08.663-1.08h.814v2.79h-.766v-1.602l-.711 1.091h-.016l-.707-1.083v1.593h-.754zm3.469 0h2.235v.658h-1.473v.422h1.334v.61h-1.334v.442h1.493v.658h-2.255zm-5.3.897l-.315.793h.624zm-1.145 5.19h8.014l-4.09 1.348z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
3
assets/images/social/fortnite.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" width="20" height="20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 3h10v3H11v3.5h5.5v3H11v8.5L7 22V3z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 242 B |
14
assets/images/social/garrysmod.svg
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<svg viewBox="0 0 384 384" xmlns="http://www.w3.org/2000/svg" width="20" height="20">
|
||||
<rect width="384" height="384" rx="60" fill="#0081FF"/>
|
||||
<path class="st1" d="M235.57,221.91c-2.84,3.4-5.68,7.51-9.22,10.88c-7.2,6.84-16.2,10.41-25.77,12.49
|
||||
c-11.53,2.51-23.13,2.18-34.76,0.61c-14.52-1.95-27.27-7.83-38.38-17.22c-9.35-7.91-16.06-17.8-20.84-29.04
|
||||
c-4.88-11.46-7.4-23.51-8.68-35.81c-1.85-17.78-0.99-35.42,4.33-52.6c4.37-14.11,11.01-27,21.08-38.03
|
||||
C135,60.4,149.18,52.08,166.1,48.59c9.46-1.95,19.01-1.95,28.57-0.4c17,2.77,30.13,11.49,39.75,25.68c0.25,0.32,1.19,1.72,1.19,1.72
|
||||
s0-4.78,0-6.83c-0.01-5.16,0-16.51,0-16.51s1.09,0,1.35,0c15.28-0.05,49.97-0.01,49.97-0.01s-0.01,1.96-0.01,4.28
|
||||
c0,64.4,0.09,128.8-0.05,193.19c-0.04,20.36-5.79,39-18.84,54.95c-10.15,12.4-23.25,20.57-38.25,25.85
|
||||
c-14.6,5.14-29.7,6.91-45.08,6.26c-16.81-0.71-32.99-4.35-47.75-12.64c-22.22-12.49-35.72-31.22-39.07-56.78
|
||||
c-0.45-3.39-0.56-6.84-0.69-10.26c-0.01-0.21-0.07-1.31-0.07-1.31s1.04,0,1.26,0c15.24-0.07,30.48-0.08,45.72-0.03
|
||||
c0.18,0,0.9,0.01,0.9,0.01s-0.01,0.96-0.01,1.12c0.16,8.86,2.62,16.86,8.77,23.44c4.59,4.91,10.26,8.16,16.7,9.89
|
||||
c14.96,4.03,29.59,3.19,43.49-3.98c12.5-6.45,20.4-16.54,21.3-30.85c0.67-10.79,0.27-21.66,0.34-32.49
|
||||
C235.57,222.35,235.57,221.79,235.57,221.91z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/images/social/hytale.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
8
assets/images/social/minecraft.svg
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" width="20" height="20">
|
||||
<rect x="0" y="0" width="100" height="100" fill="#2d1b33"/>
|
||||
<rect x="20" y="21" width="20" height="19" fill="white"/>
|
||||
<rect x="60" y="21" width="20" height="19" fill="white"/>
|
||||
<rect x="40" y="41" width="20" height="10" fill="white"/>
|
||||
<rect x="30" y="51" width="10" height="29" fill="white"/>
|
||||
<rect x="60" y="51" width="10" height="29" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 454 B |
BIN
assets/images/team/1151495567256584253.png
Normal file
|
After Width: | Height: | Size: 9 KiB |
BIN
assets/images/team/1324439906046574693.png
Normal file
|
After Width: | Height: | Size: 300 KiB |
BIN
assets/images/team/353942721997832195.gif
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
assets/images/team/534003230435442690.png
Normal file
|
After Width: | Height: | Size: 317 KiB |
BIN
assets/images/team/561485865562406914.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
assets/images/team/971446412690722826.png
Normal file
|
After Width: | Height: | Size: 384 KiB |
13
assets/js/feather.min.js
vendored
Normal file
589
assets/js/home.js
Normal file
|
|
@ -0,0 +1,589 @@
|
|||
// Home page specific JS: team carousel + modal + portfolio preview + partners + konami
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// ── Portfolio preview — même cartes que la page réalisations ──────────────────
|
||||
const homePortfolioGrid = document.getElementById('home-portfolio-grid');
|
||||
if (homePortfolioGrid) {
|
||||
fetch('assets/data/projects.json')
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||||
return res.json();
|
||||
})
|
||||
.then(projects => {
|
||||
// Garde-fou: vérifier que projects est un tableau
|
||||
if (!Array.isArray(projects)) {
|
||||
console.error('[Portfolio] Données invalides: projects n’est pas un tableau', projects);
|
||||
throw new Error('Invalid projects data format');
|
||||
}
|
||||
const first3 = projects.slice(0, 3);
|
||||
const delayClasses = ['', ' reveal-delay-1', ' reveal-delay-2'];
|
||||
const categoryLabels = { bot: 'Bot Discord', map: 'Fortnite Map', graphisme: 'Graphisme' };
|
||||
|
||||
homePortfolioGrid.innerHTML = first3.map((p, i) => {
|
||||
const firstFile = Array.isArray(p.files) && p.files.length > 0 ? p.files[0] : null;
|
||||
const imgSrc = firstFile ? window.escapeHTML((p.images || '') + firstFile) : null;
|
||||
const safeTitle = window.escapeHTML(p.title);
|
||||
const safeCategory = window.escapeHTML(categoryLabels[p.category] || p.category);
|
||||
return `
|
||||
<div class="portfolio-item reveal${delayClasses[i]}" data-project-id="${window.escapeHTML(p.id)}" role="button" tabindex="0" aria-label="Voir le projet ${safeTitle}">
|
||||
<div class="portfolio-bg" style="background: ${window.escapeHTML(p.gradient)};">
|
||||
${imgSrc ? `<img src="${imgSrc}" alt="${safeTitle}" class="portfolio-cover-img" onerror="this.style.display='none'">` : ''}
|
||||
</div>
|
||||
<div class="portfolio-content">
|
||||
<span class="portfolio-tag">${safeCategory}</span>
|
||||
<h4>${safeTitle}</h4>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
setTimeout(() => {
|
||||
homePortfolioGrid.querySelectorAll('.reveal').forEach(el => el.classList.add('active'));
|
||||
if (window.feather && feather.replace) try { feather.replace(); } catch(e) {}
|
||||
}, 80);
|
||||
|
||||
// Délégation globale sur document — fonctionne même si realisations.js
|
||||
// se charge après home.js (ordre des scripts defer non garanti)
|
||||
homePortfolioGrid.addEventListener('click', e => {
|
||||
const item = e.target.closest('.portfolio-item[data-project-id]');
|
||||
if (!item) return;
|
||||
const id = item.getAttribute('data-project-id');
|
||||
if (typeof window.openProjectModal === 'function') {
|
||||
window.openProjectModal(id);
|
||||
} else {
|
||||
// Retry : realisations.js pas encore initialisé, on attend
|
||||
const wait = setInterval(() => {
|
||||
if (typeof window.openProjectModal === 'function') {
|
||||
clearInterval(wait);
|
||||
window.openProjectModal(id);
|
||||
}
|
||||
}, 50);
|
||||
// Abandon après 3 secondes
|
||||
setTimeout(() => clearInterval(wait), 3000);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('[Portfolio] Échec du chargement des projets:', err);
|
||||
// Fallback: afficher un message d'erreur
|
||||
homePortfolioGrid.innerHTML = '
|
||||
<div class="alert alert-error" role="alert" style="grid-column: 1 / -1;">
|
||||
<p>⚠️ Impossible de charger les projets. Veuillez rafraîchir la page.</p>
|
||||
</div>
|
||||
';
|
||||
});
|
||||
}
|
||||
|
||||
// ── Partners — carousel + modal ─────────────────────────────────────────────
|
||||
const partnersTrack = document.getElementById('partners-track');
|
||||
const partnerModalOverlay = document.getElementById('partner-modal-overlay');
|
||||
|
||||
if (partnersTrack) {
|
||||
fetch('assets/data/partners.json')
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||||
return res.json();
|
||||
})
|
||||
.then(partners => {
|
||||
// Garde-fou: vérifier que partners est un tableau
|
||||
if (!Array.isArray(partners)) {
|
||||
console.error('[Partners] Données invalides: partners n’est pas un tableau', partners);
|
||||
throw new Error('Invalid partners data format');
|
||||
}
|
||||
if (!partners.length) {
|
||||
const section = document.getElementById('partenaires');
|
||||
if (section) section.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
// Générer les slides (tripler pour garantir boucle sans trou)
|
||||
const copies = [...partners, ...partners, ...partners];
|
||||
partnersTrack.innerHTML = copies.map((p, i) => {
|
||||
const imgSrc = window.escapeHTML(p.logo || `assets/images/partners/${p.name}.png`);
|
||||
const safeName = window.escapeHTML(p.displayName);
|
||||
const safeGradient = window.escapeHTML(p.gradient || '');
|
||||
const safeInitials = window.escapeHTML(p.displayName.substring(0,2).toUpperCase());
|
||||
return `
|
||||
<div class="partner-slide" data-partner-idx="${i % partners.length}" role="button" tabindex="0" aria-label="Voir ${safeName}">
|
||||
<div class="partner-slide-inner">
|
||||
<img src="${imgSrc}" alt="${safeName}" class="partner-logo-img"
|
||||
onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
|
||||
<div class="partner-logo-fallback" style="background:${safeGradient};display:none;">
|
||||
${safeInitials}
|
||||
</div>
|
||||
<span class="partner-slide-name">${safeName}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Défilement infini — loopWidth mesuré une seule fois après rendu complet
|
||||
const track = partnersTrack;
|
||||
let isPaused = false;
|
||||
let scrollPos = 0;
|
||||
let loopWidth = 0;
|
||||
let animFrame;
|
||||
const SPEED = 0.6;
|
||||
|
||||
function measureLoop() {
|
||||
// Mesure la largeur exacte d'un bloc (1/3 des slides)
|
||||
// On utilise offsetWidth de chaque slide réel + gap pour éviter les arrondis de scrollWidth
|
||||
const slides = Array.from(track.querySelectorAll('.partner-slide'));
|
||||
const perGroup = partners.length;
|
||||
if (slides.length < perGroup) return 0;
|
||||
// Largeur d'un groupe = somme des (offsetWidth + gap) des premiers slides
|
||||
const style = window.getComputedStyle(track);
|
||||
const gap = parseFloat(style.gap) || 24;
|
||||
let w = 0;
|
||||
for (let i = 0; i < perGroup; i++) {
|
||||
w += slides[i].offsetWidth + gap;
|
||||
}
|
||||
return Math.round(w);
|
||||
}
|
||||
|
||||
function autoScroll() {
|
||||
if (!isPaused) {
|
||||
scrollPos += SPEED;
|
||||
if (loopWidth > 0 && scrollPos >= loopWidth) {
|
||||
scrollPos -= loopWidth;
|
||||
}
|
||||
track.style.transform = `translateX(-${scrollPos}px)`;
|
||||
}
|
||||
animFrame = requestAnimationFrame(autoScroll);
|
||||
}
|
||||
|
||||
// Attendre que les slides soient rendues et mesurées
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
loopWidth = measureLoop();
|
||||
animFrame = requestAnimationFrame(autoScroll);
|
||||
});
|
||||
});
|
||||
|
||||
// Re-mesurer au resize pour rester précis
|
||||
let resizeTO;
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTO);
|
||||
resizeTO = setTimeout(() => { loopWidth = measureLoop(); }, 150);
|
||||
});
|
||||
|
||||
track.parentElement.addEventListener('mouseenter', () => { isPaused = true; });
|
||||
track.parentElement.addEventListener('mouseleave', () => { isPaused = false; });
|
||||
|
||||
// Click → modal
|
||||
track.querySelectorAll('.partner-slide').forEach(slide => {
|
||||
const open = () => {
|
||||
const idx = parseInt(slide.dataset.partnerIdx, 10);
|
||||
openPartnerModal(partners[idx]);
|
||||
};
|
||||
slide.addEventListener('click', open);
|
||||
slide.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(); } });
|
||||
});
|
||||
|
||||
function openPartnerModal(p) {
|
||||
// Garde-fou: vérifier que p est un objet valide
|
||||
if (!p || typeof p !== 'object') {
|
||||
console.error('[Partner Modal] Données partenaire invalides:', p);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
isPaused = true;
|
||||
const imgSrc = window.escapeHTML(p.logo || `assets/images/partners/${p.name}.png`);
|
||||
const logoImg = document.getElementById('partner-modal-logo-img');
|
||||
const logoInner = document.getElementById('partner-modal-logo-fallback');
|
||||
if (!logoImg || !logoInner) {
|
||||
console.error('[Partner Modal] Éléments DOM manquants pour la modal');
|
||||
return;
|
||||
}
|
||||
logoImg.src = imgSrc;
|
||||
logoImg.alt = window.escapeHTML(p.displayName || 'Partenaire');
|
||||
logoImg.style.display = 'block';
|
||||
logoInner.style.display = 'none';
|
||||
logoInner.style.background = '';
|
||||
logoInner.style.justifyContent = '';
|
||||
logoInner.textContent = '';
|
||||
// Si l'image échoue → fallback initiales dans l'octogone
|
||||
logoImg.onerror = function() {
|
||||
this.style.display = 'none';
|
||||
logoInner.style.background = p.gradient;
|
||||
logoInner.style.display = 'flex';
|
||||
logoInner.style.alignItems = 'center';
|
||||
logoInner.style.justifyContent = 'center';
|
||||
logoInner.style.fontSize = '2rem';
|
||||
logoInner.style.fontFamily = "'OmegaLarge', sans-serif";
|
||||
logoInner.style.color = 'white';
|
||||
logoInner.textContent = p.displayName.substring(0,2).toUpperCase();
|
||||
};
|
||||
document.getElementById('partner-modal-name').textContent = p.displayName;
|
||||
document.getElementById('partner-modal-desc').textContent = p.longDesc;
|
||||
document.getElementById('partner-modal-since').textContent = p.since;
|
||||
document.getElementById('partner-modal-services').innerHTML = (p.services || []).map(s => `<span class="team-modal-skill">${window.escapeHTML(s)}</span>`).join('');
|
||||
document.getElementById('partner-modal-stats').innerHTML = (p.stats || []).map(s => `
|
||||
<div class="team-modal-stat">
|
||||
<div class="team-modal-stat-value">${window.escapeHTML(s.value)}</div>
|
||||
<div class="team-modal-stat-label">${window.escapeHTML(s.label)}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
const links = document.getElementById('partner-modal-links');
|
||||
// Réseaux sociaux depuis le champ social du partenaire
|
||||
// Sécurité: fallback sûr sans injection HTML si renderSocialLinks n'existe pas ou est invalide
|
||||
let socialHtml = '';
|
||||
try {
|
||||
if (typeof window.renderSocialLinks === 'function') {
|
||||
// Vérification supplémentaire: s'assurer que la fonction est celle attendue (définie dans main.js)
|
||||
if (window.renderSocialLinks.toString().includes('escapeHTML')) {
|
||||
socialHtml = window.renderSocialLinks(p.social);
|
||||
} else {
|
||||
console.warn('[Partner Modal] window.renderSocialLinks semble compromise, utilisation du fallback sécurisé');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Partner Modal] Erreur lors du rendu des liens sociaux:', e);
|
||||
}
|
||||
links.innerHTML = socialHtml;
|
||||
if (window.feather) feather.replace();
|
||||
partnerModalOverlay.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
} catch (err) {
|
||||
console.error('[Partner Modal] Erreur lors de l\'ouverture de la modal:', err);
|
||||
// Fallback: fermer la modal si elle a été ouverte
|
||||
if (partnerModalOverlay) {
|
||||
partnerModalOverlay.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
}
|
||||
|
||||
if (partnerModalOverlay) {
|
||||
const closeBtn = document.getElementById('partner-modal-close');
|
||||
closeBtn && closeBtn.addEventListener('click', closePartnerModal);
|
||||
partnerModalOverlay.addEventListener('click', e => { if (e.target === partnerModalOverlay) closePartnerModal(); });
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape' && partnerModalOverlay.classList.contains('active')) closePartnerModal();
|
||||
});
|
||||
}
|
||||
|
||||
function closePartnerModal() {
|
||||
partnerModalOverlay.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
isPaused = false;
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('[Partners] Échec du chargement des partenaires:', err);
|
||||
const section = document.getElementById('partenaires');
|
||||
if (section) {
|
||||
section.innerHTML = '
|
||||
<div class="alert alert-error" role="alert">
|
||||
<p>⚠️ Impossible de charger les partenaires. Veuillez réessayer plus tard.</p>
|
||||
</div>
|
||||
';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Team section ─────────────────────────────────────────────────────────────
|
||||
|
||||
fetch('assets/data/team.json')
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||||
return res.json();
|
||||
})
|
||||
.then(members => {
|
||||
try {
|
||||
if (!Array.isArray(members)) {
|
||||
console.error('[Team] Données invalides: members n’est pas un tableau', members);
|
||||
throw new Error('Invalid team data format');
|
||||
}
|
||||
initTeamSection(members);
|
||||
} catch (err) {
|
||||
console.error('[Team] Erreur lors de l’initialisation de la section créateurs:', err);
|
||||
// Fallback: afficher un message d'erreur dans la section si elle existe
|
||||
const teamSection = document.getElementById('les-createurs') || document.getElementById('team');
|
||||
if (teamSection) {
|
||||
teamSection.innerHTML = '
|
||||
<div class="alert alert-error" role="alert">
|
||||
<p>⚠️ Impossible de charger les informations de l\'équipe. Veuillez rafraîchir la page.</p>
|
||||
</div>
|
||||
';
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('[Team] Échec du chargement des données de l’équipe:', err);
|
||||
// Fallback: afficher un message d'erreur dans la section si elle existe
|
||||
const teamSection = document.getElementById('les-createurs') || document.getElementById('team');
|
||||
if (teamSection) {
|
||||
teamSection.innerHTML = '
|
||||
<div class="alert alert-error" role="alert">
|
||||
<p>⚠️ Impossible de charger les informations de l\'équipe. Veuillez réessayer plus tard.</p>
|
||||
</div>
|
||||
';
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Konami + easter egg → définis au niveau top-level du script (voir bas de fichier)
|
||||
|
||||
// ── initTeamSection ──────────────────────────────────────────────────────────
|
||||
function initTeamSection(members) {
|
||||
// Sécurité: les garde-fous empêchent une erreur JS de casser tout le rendu.
|
||||
console.log('[Team] Initialisation de la section créateurs avec', members.length, 'membres');
|
||||
|
||||
if (!Array.isArray(members)) {
|
||||
console.error('[Team] initTeamSection: members n’est pas un tableau');
|
||||
return;
|
||||
}
|
||||
|
||||
const carousel = document.getElementById('team-carousel');
|
||||
const dotsContainer = document.getElementById('carousel-dots');
|
||||
const prevBtn = document.getElementById('carousel-prev');
|
||||
const nextBtn = document.getElementById('carousel-next');
|
||||
|
||||
if (!carousel) {
|
||||
console.error('[Team] initTeamSection: #team-carousel introuvable');
|
||||
return;
|
||||
}
|
||||
if (!dotsContainer) {
|
||||
console.warn('[Team] initTeamSection: #carousel-dots introuvable (dots désactivés)');
|
||||
}
|
||||
|
||||
// escapeHTML est utilisé pour éviter l’injection XSS via innerHTML.
|
||||
if (typeof window.escapeHTML !== 'function') {
|
||||
console.error('[Team] initTeamSection: window.escapeHTML introuvable (risque XSS)');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
carousel.innerHTML = members.map((m, i) => {
|
||||
const imgSrc = window.escapeHTML(`assets/images/team/${m.id}.png`);
|
||||
const safeName = window.escapeHTML(m.name);
|
||||
const safeRole = window.escapeHTML(m.role);
|
||||
const safeBio = window.escapeHTML((m.bio || '').split('.').shift() + '.');
|
||||
const safeFallback = window.escapeHTML(m.avatar || '');
|
||||
return `
|
||||
<div class="team-card" data-member="${i}" role="button" tabindex="0" aria-label="Voir le profil de ${safeName}">
|
||||
<div class="team-avatar">
|
||||
<div class="team-avatar-inner" id="team-avatar-inner-${i}">
|
||||
<img src="${imgSrc}" alt="${safeName}" class="team-avatar-photo"
|
||||
onerror="const s=this.src;if(s.endsWith('.png')){this.src=s.replace('.png','.jpg');}else if(s.endsWith('.jpg')){this.src=s.replace('.jpg','.gif');}else{this.style.display='none';document.getElementById('team-avatar-fallback-${i}').style.display='flex';}">
|
||||
<span class="team-avatar-fallback" id="team-avatar-fallback-${i}" style="display:none;">${safeFallback}</span>
|
||||
</div>
|
||||
</div>
|
||||
<h4>${safeName}</h4>
|
||||
<p class="team-role">${safeRole}</p>
|
||||
<p class="team-desc">${safeBio}</p>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
let cards = Array.from(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 getTotalDots() { return Math.max(1, cards.length - cardsPerView + 1); }
|
||||
|
||||
function createDots() {
|
||||
if (!dotsContainer) return;
|
||||
dotsContainer.innerHTML = '';
|
||||
for (let i = 0; i < getTotalDots(); 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 = Array.from(carousel.querySelectorAll('.team-card'));
|
||||
if (!cards.length) return;
|
||||
const gap = 32;
|
||||
const cardWidth = cards[0].offsetWidth + gap;
|
||||
carousel.style.transform = `translateX(-${currentIndex * cardWidth}px)`;
|
||||
if (dotsContainer) Array.from(dotsContainer.querySelectorAll('.carousel-dot')).forEach((d, i) => d.classList.toggle('active', i === currentIndex));
|
||||
}
|
||||
|
||||
function goToSlide(index) {
|
||||
currentIndex = Math.min(index, Math.max(0, cards.length - cardsPerView));
|
||||
updateCarousel(); resetAutoPlay();
|
||||
}
|
||||
|
||||
function nextSlide() {
|
||||
const max = Math.max(0, cards.length - cardsPerView);
|
||||
currentIndex = currentIndex >= max ? 0 : currentIndex + 1;
|
||||
updateCarousel();
|
||||
}
|
||||
|
||||
function prevSlide() {
|
||||
const max = Math.max(0, cards.length - cardsPerView);
|
||||
currentIndex = currentIndex <= 0 ? max : currentIndex - 1;
|
||||
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);
|
||||
|
||||
let resizeTimer;
|
||||
const ro = new ResizeObserver(() => {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(() => { updateCardsPerView(); currentIndex = 0; createDots(); updateCarousel(); }, 100);
|
||||
});
|
||||
ro.observe(carousel.parentElement || document.body);
|
||||
|
||||
updateCardsPerView(); createDots(); updateCarousel(); startAutoPlay();
|
||||
|
||||
// Modal
|
||||
const modalOverlay = document.getElementById('team-modal-overlay');
|
||||
const modalClose = document.getElementById('team-modal-close');
|
||||
const modalAvatar = document.getElementById('team-modal-avatar');
|
||||
const modalName = document.getElementById('team-modal-name');
|
||||
const modalRole = document.getElementById('team-modal-role');
|
||||
const modalBio = document.getElementById('team-modal-bio');
|
||||
const modalStats = document.getElementById('team-modal-stats');
|
||||
const modalSkills = document.getElementById('team-modal-skills');
|
||||
const modalSocial = document.getElementById('team-modal-social');
|
||||
|
||||
function openMemberModal(index) {
|
||||
const m = members[index];
|
||||
// Garde-fou: vérifier que m est un objet valide
|
||||
if (!m || typeof m !== 'object') {
|
||||
console.error('[Team Modal] Données membre invalides pour l\'index', index, ':', m);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Avatar avec image si dispo
|
||||
const imgSrc = window.escapeHTML(`assets/images/team/${m.id}.png`);
|
||||
const safeName = window.escapeHTML(m.name);
|
||||
const safeFallback = window.escapeHTML(m.avatar || (m.name ? m.name.charAt(0) : '?'));
|
||||
if (!modalAvatar) {
|
||||
console.error('[Team Modal] Éléments DOM manquants pour la modal');
|
||||
return;
|
||||
}
|
||||
modalAvatar.innerHTML = `
|
||||
<img src="${imgSrc}" alt="${safeName}" class="team-modal-avatar-photo"
|
||||
onerror="const s=this.src;if(s.endsWith('.png')){this.src=s.replace('.png','.jpg');}else if(s.endsWith('.jpg')){this.src=s.replace('.jpg','.gif');}else{this.style.display='none';this.nextElementSibling.style.display='flex';}">
|
||||
<span class="team-modal-avatar-fallback">${safeFallback}</span>
|
||||
`;
|
||||
modalName.textContent = m.name;
|
||||
modalRole.textContent = m.role;
|
||||
modalBio.textContent = m.bio;
|
||||
modalStats.innerHTML = (m.stats || []).map(s => `
|
||||
<div class="team-modal-stat">
|
||||
<div class="team-modal-stat-value">${window.escapeHTML(s.value)}</div>
|
||||
<div class="team-modal-stat-label">${window.escapeHTML(s.label)}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
modalSkills.innerHTML = (m.skills || []).map(s => `<span class="team-modal-skill">${window.escapeHTML(s)}</span>`).join('');
|
||||
|
||||
// Réseaux sociaux depuis le JSON du membre (+ fallback discord global)
|
||||
// Sécurité : ne jamais injecter de HTML non maîtrisé si le renderer n'existe pas ou est compromis.
|
||||
let socialHtml = '';
|
||||
try {
|
||||
if (typeof window.renderSocialLinks === 'function') {
|
||||
// Vérification supplémentaire: s'assurer que la fonction utilise escapeHTML
|
||||
if (window.renderSocialLinks.toString().includes('escapeHTML')) {
|
||||
socialHtml = window.renderSocialLinks(m.social, (window.OK_SOCIAL || {}).discord);
|
||||
} else {
|
||||
console.warn('[Team Modal] window.renderSocialLinks semble compromise, utilisation du fallback sécurisé');
|
||||
}
|
||||
} else {
|
||||
console.warn('[Team Modal] window.renderSocialLinks non définie');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Team Modal] Erreur lors du rendu des liens sociaux:', e);
|
||||
}
|
||||
modalSocial.innerHTML = socialHtml;
|
||||
|
||||
modalOverlay.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
} catch (err) {
|
||||
console.error('[Team Modal] Erreur lors de l\'ouverture de la modal:', err);
|
||||
// Fallback: fermer la modal si elle a été ouverte
|
||||
if (modalOverlay) {
|
||||
modalOverlay.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
}
|
||||
|
||||
function closeMemberModal() {
|
||||
modalOverlay.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
carousel.querySelectorAll('.team-card').forEach(card => {
|
||||
const openCard = () => { const idx = parseInt(card.dataset.member, 10); openMemberModal(idx); };
|
||||
card.addEventListener('click', openCard);
|
||||
card.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openCard(); } });
|
||||
});
|
||||
|
||||
modalClose && modalClose.addEventListener('click', closeMemberModal);
|
||||
modalOverlay && modalOverlay.addEventListener('click', e => { if (e.target === modalOverlay) closeMemberModal(); });
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape' && modalOverlay && modalOverlay.classList.contains('active')) closeMemberModal(); });
|
||||
}
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// EASTER EGG — top-level (hors DOMContentLoaded pour garantir l'attachement)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
(function () {
|
||||
var KONAMI = ['ArrowUp','ArrowUp','ArrowDown','ArrowDown','ArrowLeft','ArrowRight','ArrowLeft','ArrowRight','b','a'];
|
||||
var progress = 0;
|
||||
|
||||
// PC : Konami Code — capture:true pour passer avant tout autre listener
|
||||
window.addEventListener('keydown', function(e) {
|
||||
var key = e.key.length === 1 ? e.key.toLowerCase() : e.key;
|
||||
if (key === KONAMI[progress]) {
|
||||
progress++;
|
||||
if (progress === KONAMI.length) {
|
||||
progress = 0;
|
||||
triggerEasterEgg();
|
||||
}
|
||||
} else {
|
||||
progress = (key === KONAMI[0]) ? 1 : 0;
|
||||
}
|
||||
}, true); // capture phase — reçu avant tout stopPropagation
|
||||
|
||||
// Mobile : 10 taps sur le logo navbar OU logo footer
|
||||
var tapCount = 0;
|
||||
var tapTimer = null;
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!e.target.closest('.logo') && !e.target.closest('.logo-mini') && !e.target.closest('.omega-symbol')) return;
|
||||
if (!('ontouchstart' in window) && navigator.maxTouchPoints === 0) return;
|
||||
tapCount++;
|
||||
clearTimeout(tapTimer);
|
||||
tapTimer = setTimeout(function() { tapCount = 0; }, 600);
|
||||
if (tapCount >= 10) { tapCount = 0; triggerEasterEgg(); }
|
||||
});
|
||||
|
||||
function triggerEasterEgg() {
|
||||
if (document.getElementById('easter-egg-overlay')) return;
|
||||
var egg = document.createElement('div');
|
||||
egg.id = 'easter-egg-overlay';
|
||||
egg.setAttribute('role', 'dialog');
|
||||
egg.setAttribute('aria-modal', 'true');
|
||||
egg.innerHTML = '<div class="easter-egg-box"><div class="easter-egg-icon">🎮</div><h2>GG\u00a0! Tu as trouvé l\u2019easter egg\u00a0!</h2><p>La vraie question c\u2019est\u00a0: combien de temps tu as mis\u00a0?</p><button id="easter-egg-close" class="btn btn-primary">Fermer</button></div>';
|
||||
document.body.appendChild(egg);
|
||||
var closeBtn = document.getElementById('easter-egg-close');
|
||||
closeBtn && closeBtn.focus();
|
||||
var remove = function() { egg.remove(); };
|
||||
closeBtn && closeBtn.addEventListener('click', remove);
|
||||
egg.addEventListener('click', function(e) { if (e.target === egg) remove(); });
|
||||
window.addEventListener('keydown', function esc(e) {
|
||||
if (e.key === 'Escape') { remove(); window.removeEventListener('keydown', esc); }
|
||||
}, { once: true });
|
||||
}
|
||||
})();
|
||||
514
assets/js/main.js
Normal file
|
|
@ -0,0 +1,514 @@
|
|||
// OmégaKube - shared scripts
|
||||
|
||||
/**
|
||||
* Sécurise une chaîne contre les attaques XSS avant insertion dans le DOM (innerHTML)
|
||||
*/
|
||||
window.escapeHTML = function(str) {
|
||||
if (str === null || str === undefined) return '';
|
||||
if (typeof str !== 'string') str = String(str);
|
||||
return str.replace(/[&<>'"]/g, function(tag) {
|
||||
const charsToReplace = { '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' };
|
||||
return charsToReplace[tag] || tag;
|
||||
});
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// CONFIGURATION GLOBALE — liens réseaux sociaux
|
||||
// Modifier ici pour mettre à jour partout sur le site
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
window.OK_SOCIAL = {
|
||||
discord: 'https://discord.gg/FwvNkvu5dy',
|
||||
fortnite: 'https://www.fortnite.com/@omega_kube_team',
|
||||
twitter: 'https://x.com/Omegakube',
|
||||
instagram: 'https://www.instagram.com/omegakube/',
|
||||
youtube: 'https://www.youtube.com/@OmegaKube',
|
||||
tiktok: 'https://www.tiktok.com/@omegakube',
|
||||
facebook: '#',
|
||||
bluesky: '#',
|
||||
epicgames: 'https://www.fortnite.com/@omega_kube_team',
|
||||
email: 'contact@omegakube.fr'
|
||||
};
|
||||
|
||||
// SVGs des réseaux — utilisé partout dans les modals
|
||||
const SVG_ICONS = {
|
||||
discord: '<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>',
|
||||
fortnite: '<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M2.234 0v24h6.522v-8.035h5.9v-5.64h-5.9V5.636h7.964V0zm11.13 10.329v5.636h5.9v2.382h-5.9V24H21.766V10.329z"/></svg>',
|
||||
epicgames: '<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M3.537 0C2.165 0 1.66.506 1.66 1.879V18.44a4.262 4.262 0 00.02.433c.031.3.037.59.316.92.027.033.311.245.311.245.153.075.258.13.43.2l8.335 3.491c.433.199.614.276.928.27h.002c.314.006.495-.071.928-.27l8.335-3.492c.172-.07.277-.124.43-.2 0 0 .284-.211.311-.243.28-.33.285-.621.316-.92a4.261 4.261 0 00.02-.434V1.879c0-1.373-.506-1.88-1.878-1.88zm13.366 3.11h.68c1.138 0 1.688.553 1.688 1.696v1.88h-1.374v-1.8c0-.369-.17-.54-.523-.54h-.235c-.367 0-.537.17-.537.539v5.81c0 .369.17.54.537.54h.262c.353 0 .523-.171.523-.54V8.619h1.373v2.143c0 1.144-.562 1.71-1.7 1.71h-.694c-1.138 0-1.7-.566-1.7-1.71V4.82c0-1.144.562-1.709 1.7-1.709zm-12.186.08h3.114v1.274H6.117v2.603h1.648v1.275H6.117v2.774h1.74v1.275h-3.14zm3.816 0h2.198c1.138 0 1.7.564 1.7 1.708v2.445c0 1.144-.562 1.71-1.7 1.71h-.799v3.338h-1.4zm4.53 0h1.4v9.201h-1.4zm-3.13 1.235v3.392h.575c.354 0 .523-.171.523-.54V4.965c0-.368-.17-.54-.523-.54zm-3.74 10.147a1.708 1.708 0 01.591.108 1.745 1.745 0 01.49.299l-.452.546a1.247 1.247 0 00-.308-.195.91.91 0 00-.363-.068.658.658 0 00-.28.06.703.703 0 00-.224.163.783.783 0 00-.151.243.799.799 0 00-.056.299v.008a.852.852 0 00.056.31.7.7 0 00.157.245.736.736 0 00.238.16.774.774 0 00.303.058.79.79 0 00.445-.116v-.339h-.548v-.565H7.37v1.255a2.019 2.019 0 01-.524.307 1.789 1.789 0 01-.683.123 1.642 1.642 0 01-.602-.107 1.46 1.46 0 01-.478-.3 1.371 1.371 0 01-.318-.455 1.438 1.438 0 01-.115-.58v-.008a1.426 1.426 0 01.113-.57 1.449 1.449 0 01.312-.46 1.418 1.418 0 01.474-.309 1.58 1.58 0 01.598-.111 1.708 1.708 0 01.045 0zm11.963.008a2.006 2.006 0 01.612.094 1.61 1.61 0 01.507.277l-.386.546a1.562 1.562 0 00-.39-.205 1.178 1.178 0 00-.388-.07.347.347 0 00-.208.052.154.154 0 00-.07.127v.008a.158.158 0 00.022.084.198.198 0 00.076.066.831.831 0 00.147.06c.062.02.14.04.236.061a3.389 3.389 0 01.43.122 1.292 1.292 0 01.328.17.678.678 0 01.207.24.739.739 0 01.071.337v.008a.865.865 0 01-.081.382.82.82 0 01-.229.285 1.032 1.032 0 01-.353.18 1.606 1.606 0 01-.46.061 2.16 2.16 0 01-.71-.116 1.718 1.718 0 01-.593-.346l.43-.514c.277.223.578.335.9.335a.457.457 0 00.236-.05.157.157 0 00.082-.142v-.008a.15.15 0 00-.02-.077.204.204 0 00-.073-.066.753.753 0 00-.143-.062 2.45 2.45 0 00-.233-.062 5.036 5.036 0 01-.413-.113 1.26 1.26 0 01-.331-.16.72.72 0 01-.222-.243.73.73 0 01-.082-.36v-.008a.863.863 0 01.074-.359.794.794 0 01.214-.283 1.007 1.007 0 01.34-.185 1.423 1.423 0 01.448-.066 2.006 2.006 0 01.025 0zm-9.358.025h.742l1.183 2.81h-.825l-.203-.499H8.623l-.198.498h-.81zm2.197.02h.814l.663 1.08.663-1.08h.814v2.79h-.766v-1.602l-.711 1.091h-.016l-.707-1.083v1.593h-.754zm3.469 0h2.235v.658h-1.473v.422h1.334v.61h-1.334v.442h1.493v.658h-2.255zm-5.3.897l-.315.793h.624zm-1.145 5.19h8.014l-4.09 1.348z"/></svg>',
|
||||
minecraft: '<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M3.261 0A3.261 3.261 0 0 0 0 3.261V20.74A3.261 3.261 0 0 0 3.261 24H20.74A3.261 3.261 0 0 0 24 20.739V3.261A3.261 3.261 0 0 0 20.739 0zm4.103 3.836h3.27v3.268h3.268V3.836h3.27v3.268h-3.27v3.27h3.27v3.267h-3.27v3.27h-3.268v-3.27H7.364v-3.268h3.268V7.104H7.364z"/></svg>',
|
||||
gmod: '<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm0 2c5.514 0 10 4.486 10 10s-4.486 10-10 10S2 17.514 2 12 6.486 2 12 2zm1 4h-2v6H7v2h4v4h2v-4h4v-2h-4z"/></svg>',
|
||||
hytale: '<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M1.117 0v24h5.412v-9.386h10.942V24h5.412V0h-5.412v9.386H6.529V0z"/></svg>',
|
||||
twitter: '<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.746l7.73-8.835L1.254 2.25H8.08l4.259 5.631 5.905-5.631zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>',
|
||||
instagram: '<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/></svg>',
|
||||
youtube: '<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>',
|
||||
tiktok: '<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M19.59 6.69a4.83 4.83 0 0 1-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 0 1-2.88 2.5 2.89 2.89 0 0 1-2.89-2.89 2.89 2.89 0 0 1 2.89-2.89c.28 0 .54.04.79.1V9.01a6.32 6.32 0 0 0-.79-.05 6.34 6.34 0 0 0-6.34 6.34 6.34 6.34 0 0 0 6.34 6.34 6.34 6.34 0 0 0 6.33-6.34V8.69a8.17 8.17 0 0 0 4.77 1.52V6.75a4.85 4.85 0 0 1-1-.06z"/></svg>',
|
||||
github: '<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0 1 12 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z"/></svg>',
|
||||
website: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>',
|
||||
};
|
||||
|
||||
/**
|
||||
* Génère le HTML des liens sociaux pour les modals
|
||||
* @param {Object} social - objet {discord, twitter, instagram, ...}
|
||||
* @param {string} [discordFallback] - lien discord global si pas de lien perso
|
||||
* @returns {string} HTML des icônes sociales
|
||||
*/
|
||||
window.renderSocialLinks = function(social, discordFallback) {
|
||||
const S = social || {};
|
||||
const entries = [
|
||||
{ key: 'discord', url: S.discord || discordFallback },
|
||||
{ key: 'fortnite', url: S.fortnite },
|
||||
{ key: 'twitter', url: S.twitter },
|
||||
{ key: 'instagram', url: S.instagram },
|
||||
{ key: 'youtube', url: S.youtube },
|
||||
{ key: 'tiktok', url: S.tiktok },
|
||||
{ key: 'epicgames', url: S.epicgames },
|
||||
{ key: 'github', url: S.github },
|
||||
{ key: 'website', url: S.website },
|
||||
];
|
||||
return entries
|
||||
.filter(e => e.url && e.url !== '#' && e.url !== '' && (e.url.startsWith('https://') || e.url.startsWith('mailto:')))
|
||||
.map(e => `<a href="${window.escapeHTML(e.url)}" target="_blank" rel="noopener noreferrer" aria-label="${window.escapeHTML(e.key)}">${SVG_ICONS[e.key] || ''}</a>`)
|
||||
.join('');
|
||||
};
|
||||
|
||||
// Feather icons loader (centralized)
|
||||
// loads local feather-icons when needed and exposes window.ensureFeather()
|
||||
const FEATHER_URL = window.location.pathname.includes('/pages/') ? '../assets/js/feather.min.js' : 'assets/js/feather.min.js';
|
||||
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 {
|
||||
// Sécurité: on résout l'URL relative à partir du document courant,
|
||||
// puis on autorise uniquement les fichiers du même origin.
|
||||
// Objectif: éviter les cas où un pattern trop strict bloque certains includes relatifs (ex: "../components/..."),
|
||||
// tout en refusant les contenus externes.
|
||||
const resolvedUrl = new URL(url, document.baseURI);
|
||||
const origin = new URL(window.location.href).origin;
|
||||
if (resolvedUrl.origin !== origin) {
|
||||
console.error('Blocked potentially unsafe include origin:', resolvedUrl.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
// On autorise uniquement HTTP(s) et on évite les schémas bizarres.
|
||||
if (!/^https?$/i.test(resolvedUrl.protocol.replace(':',''))) {
|
||||
console.error('Blocked include with unsupported protocol:', resolvedUrl.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch(resolvedUrl.toString());
|
||||
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);
|
||||
});
|
||||
});
|
||||
// Eviter de remplacer par un DocumentFragment (comportement variable selon navigateurs)
|
||||
// On remplace plutôt par les nœuds enfants réels.
|
||||
el.replaceWith(...Array.from(fragment.childNodes));
|
||||
} 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();
|
||||
|
||||
// Hydrate les liens sociaux (data-social) avec les URLs de OK_SOCIAL
|
||||
(function hydrateSocialLinks() {
|
||||
const S = window.OK_SOCIAL || {};
|
||||
const map = {
|
||||
'discord': S.discord,
|
||||
'twitter': S.twitter,
|
||||
'instagram': S.instagram,
|
||||
'youtube': S.youtube,
|
||||
'tiktok': S.tiktok,
|
||||
'facebook': S.facebook,
|
||||
'bluesky': S.bluesky,
|
||||
'email-link': S.email ? 'mailto:' + S.email : '#',
|
||||
'epicgames': S.epicgames,
|
||||
};
|
||||
document.querySelectorAll('[data-social]').forEach(el => {
|
||||
const key = el.getAttribute('data-social');
|
||||
if (map[key] && map[key] !== '#') {
|
||||
el.setAttribute('href', map[key]);
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
// Déclenche les reveals maintenant que tous les includes sont dans le DOM
|
||||
setTimeout(() => {
|
||||
document.querySelectorAll('.reveal:not(.active)').forEach(el => {
|
||||
if (el.getBoundingClientRect().top < window.innerHeight - 50) {
|
||||
el.classList.add('active');
|
||||
}
|
||||
});
|
||||
}, 0);
|
||||
|
||||
const html = document.documentElement;
|
||||
|
||||
function getPreferredTheme() {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
if (['light', 'dark'].includes(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 — re-query après chargement des includes
|
||||
function revealOnScroll() {
|
||||
const els = document.querySelectorAll('.reveal:not(.active)');
|
||||
els.forEach(el => {
|
||||
if (el.getBoundingClientRect().top < window.innerHeight - 50) {
|
||||
el.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
// Déclenche immédiatement après includes + au scroll + au load
|
||||
revealOnScroll();
|
||||
window.addEventListener('scroll', revealOnScroll, { passive: true });
|
||||
window.addEventListener('load', revealOnScroll);
|
||||
|
||||
// Contact form (if exists) c'est en double
|
||||
// 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
|
|
@ -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);
|
||||
}
|
||||
35
assets/js/pages/legal.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// 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')) {
|
||||
const saved = localStorage.getItem('theme');
|
||||
const validTheme = ['dark', 'light'].includes(saved) ? saved : 'dark';
|
||||
html.setAttribute('data-theme', validTheme);
|
||||
}
|
||||
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');
|
||||
});
|
||||
});
|
||||
450
assets/js/pages/realisations.js
Normal file
|
|
@ -0,0 +1,450 @@
|
|||
// Page script for /pages/realisations.html et index.html
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Fonctionne depuis /pages/ et depuis / (index.html)
|
||||
const isRoot = !window.location.pathname.includes('/pages/');
|
||||
const dataUrl = isRoot ? 'assets/data/projects.json' : '../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() {
|
||||
if (!portfolioGrid) return; // pas sur la page réalisations
|
||||
const filtered = getFilteredProjects();
|
||||
const projects = getProjectsForPage(filtered, currentPage);
|
||||
|
||||
portfolioGrid.innerHTML = projects.map(project => {
|
||||
const prefix = isRoot ? '' : '../';
|
||||
const firstFile = Array.isArray(project.files) && project.files.length > 0 ? project.files[0] : null;
|
||||
const safeTitle = window.escapeHTML(project.title);
|
||||
const coverImg = firstFile ? `<img src="${window.escapeHTML(prefix + project.images + firstFile)}" alt="${safeTitle}" class="portfolio-cover-img" onerror="this.style.display='none'">` : '';
|
||||
return `
|
||||
<div class="portfolio-item reveal" data-project-id="${window.escapeHTML(project.id)}">
|
||||
<div class="portfolio-bg" style="background: ${window.escapeHTML(project.gradient)};">${coverImg}</div>
|
||||
<div class="portfolio-content">
|
||||
<span class="portfolio-tag">${window.escapeHTML(getCategoryLabel(project.category))}</span>
|
||||
<h4>${safeTitle}</h4>
|
||||
</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() {
|
||||
if (!paginationContainer) return; // pas sur la page réalisations
|
||||
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' });
|
||||
}
|
||||
|
||||
// ── Modal tab switching ──────────────────────────────────────────────────────
|
||||
const modalTabs = document.querySelectorAll('.project-modal-tab');
|
||||
modalTabs.forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
const tabId = tab.dataset.tab;
|
||||
modalTabs.forEach(t => t.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
document.querySelectorAll('.project-modal-tab-content').forEach(c => {
|
||||
c.classList.toggle('active', c.id === 'tab-' + tabId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function resetModalTabs() {
|
||||
modalTabs.forEach((t, i) => t.classList.toggle('active', i === 0));
|
||||
document.querySelectorAll('.project-modal-tab-content').forEach((c, i) => c.classList.toggle('active', i === 0));
|
||||
}
|
||||
|
||||
function openProjectModal(projectId) {
|
||||
const project = allProjects.find(p => p.id === Number(projectId));
|
||||
if (!project) return;
|
||||
|
||||
resetModalTabs();
|
||||
|
||||
modalCategory.textContent = getCategoryLabel(project.category);
|
||||
modalTitle.textContent = project.title;
|
||||
modalDesc.textContent = project.desc;
|
||||
|
||||
modalMeta.innerHTML = `<span><i data-feather="calendar"></i> ${window.escapeHTML(project.year)}</span>${project.duration ? ` <span><i data-feather="clock"></i> ${window.escapeHTML(project.duration)}</span>` : ''}`;
|
||||
modalFeatures.innerHTML = project.features.map(f => `<div class="project-feature-item">${window.escapeHTML(f)}</div>`).join('');
|
||||
|
||||
// Tech avec icônes (support ancien format string et nouveau format {name, icon})
|
||||
modalTech.innerHTML = project.tech.map(t => {
|
||||
const name = typeof t === 'object' ? t.name : t;
|
||||
const icon = typeof t === 'object' ? (t.icon || 'code') : 'code';
|
||||
return `<div class="project-tech-item"><i data-feather="${window.escapeHTML(icon)}"></i>${window.escapeHTML(name)}</div>`;
|
||||
}).join('');
|
||||
|
||||
// Stats en grille 2×2 avec icônes
|
||||
modalStats.innerHTML = project.stats.map(s => `
|
||||
<div class="project-stat-card">
|
||||
<div class="project-stat-card-icon"><i data-feather="${window.escapeHTML(s.icon || 'star')}"></i></div>
|
||||
<div class="project-stat-card-value">${window.escapeHTML(s.value)}</div>
|
||||
<div class="project-stat-card-label">${window.escapeHTML(s.label)}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Gallery : utilise project.files (noms réels) avec fallback gradient si vide
|
||||
const imgPath = (isRoot ? '' : '../') + (project.images || '');
|
||||
const files = Array.isArray(project.files) ? project.files : [];
|
||||
|
||||
if (files.length > 0) {
|
||||
// Images réelles nommées explicitement dans le JSON
|
||||
// Supporte : fichier local, mp4 local, URL YouTube, shortcode yt:VIDEO_ID
|
||||
gallerySlides.innerHTML = files.map((filename, i) => {
|
||||
const isLocalVideo = /\.(mp4|webm|ogg)$/i.test(filename);
|
||||
|
||||
// Détecte YouTube : URL complète ou shortcode yt:ID
|
||||
const ytMatch = filename.match(/(?:yt:|youtube\.com\/watch\?v=|youtu\.be\/)([\w-]+)/);
|
||||
|
||||
if (ytMatch) {
|
||||
const videoId = ytMatch[1];
|
||||
return `
|
||||
<div class="project-gallery-slide slide-right gallery-slide-youtube" data-img-idx="${i}" style="background:${window.escapeHTML(project.gradient)};">
|
||||
<div class="yt-wrapper">
|
||||
<iframe
|
||||
src="https://www.youtube.com/embed/${window.escapeHTML(videoId)}?rel=0&modestbranding=1"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
class="gallery-slide-yt-iframe"
|
||||
title="Vidéo YouTube ${window.escapeHTML(project.title)}"
|
||||
loading="lazy">
|
||||
</iframe>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (isLocalVideo) {
|
||||
return `
|
||||
<div class="project-gallery-slide slide-right" data-img-idx="${i}" style="background:${window.escapeHTML(project.gradient)};">
|
||||
<video class="gallery-slide-img gallery-video" src="${window.escapeHTML(imgPath + filename)}" muted loop autoplay playsinline></video>
|
||||
<button class="gallery-mute-btn" aria-label="Activer le son" title="Activer le son" onclick="(function(btn){var v=btn.closest('.project-gallery-slide').querySelector('video');v.muted=!v.muted;btn.classList.toggle('unmuted',!v.muted);btn.setAttribute('aria-label',v.muted?'Activer le son':'Couper le son');})(this)">
|
||||
<svg class="icon-muted" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/></svg>
|
||||
<svg class="icon-unmuted" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="project-gallery-slide slide-right" data-img-idx="${i}" style="background:${window.escapeHTML(project.gradient)};">
|
||||
<img src="${window.escapeHTML(imgPath + filename)}" alt="${window.escapeHTML(project.title)} ${i + 1}"
|
||||
class="gallery-slide-img"
|
||||
onerror="this.parentElement.classList.add('no-image');this.style.display='none'">
|
||||
</div>`;
|
||||
}).join('');
|
||||
} else {
|
||||
// Fallback : une slide gradient unique
|
||||
gallerySlides.innerHTML = `
|
||||
<div class="project-gallery-slide has-image" style="background:${window.escapeHTML(project.gradient)};"></div>`;
|
||||
}
|
||||
|
||||
// reset gallery + affichage immédiat
|
||||
galleryIndex = 0;
|
||||
updateGallery();
|
||||
updateGalleryDots();
|
||||
|
||||
if (modalOverlay) modalOverlay.classList.add('active');
|
||||
|
||||
// Réseaux sociaux du projet
|
||||
const projectSocialEl = document.getElementById('modal-project-social');
|
||||
if (projectSocialEl) {
|
||||
// Sécurité: fallback sûr sans injection HTML si renderSocialLinks n'existe pas ou est invalide
|
||||
let socialHtml = '';
|
||||
try {
|
||||
if (typeof window.renderSocialLinks === 'function') {
|
||||
// Vérification supplémentaire: s'assurer que la fonction utilise escapeHTML
|
||||
if (window.renderSocialLinks.toString().includes('escapeHTML')) {
|
||||
socialHtml = window.renderSocialLinks(project.social);
|
||||
} else {
|
||||
console.warn('[Project Modal] window.renderSocialLinks semble compromise, utilisation du fallback sécurisé');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Project Modal] Erreur lors du rendu des liens sociaux:', e);
|
||||
}
|
||||
projectSocialEl.innerHTML = socialHtml;
|
||||
}
|
||||
|
||||
if (window.feather && feather.replace) {
|
||||
try { feather.replace(); } catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
function closeProjectModal() {
|
||||
if (modalOverlay) modalOverlay.classList.remove('active');
|
||||
}
|
||||
|
||||
// Gallery helpers
|
||||
let galleryIndex = 0;
|
||||
function getAllSlides() {
|
||||
if (!gallerySlides) return [];
|
||||
return Array.from(gallerySlides.children).filter(s => !s.classList.contains('no-image'));
|
||||
}
|
||||
|
||||
function updateGallery() {
|
||||
const slides = getAllSlides();
|
||||
if (!slides.length) return;
|
||||
if (galleryIndex >= slides.length) galleryIndex = 0;
|
||||
if (galleryIndex < 0) galleryIndex = slides.length - 1;
|
||||
slides.forEach((s, i) => {
|
||||
// Le CSS gère tout via .active — on ne touche JAMAIS à style.transform
|
||||
s.classList.toggle('active', i === galleryIndex);
|
||||
// Juste la direction (gauche/droite) via une classe data
|
||||
if (i < galleryIndex) {
|
||||
s.classList.add('slide-left');
|
||||
s.classList.remove('slide-right');
|
||||
} else if (i > galleryIndex) {
|
||||
s.classList.add('slide-right');
|
||||
s.classList.remove('slide-left');
|
||||
} else {
|
||||
s.classList.remove('slide-left', 'slide-right');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateGalleryDots() {
|
||||
const galleryDots = document.getElementById('gallery-dots');
|
||||
if (!galleryDots) return;
|
||||
const slides = getAllSlides();
|
||||
galleryDots.innerHTML = slides.map((_, i) =>
|
||||
`<div class="gallery-dot${i === 0 ? ' active' : ''}" data-idx="${i}"></div>`
|
||||
).join('');
|
||||
galleryDots.querySelectorAll('.gallery-dot').forEach(dot => {
|
||||
dot.addEventListener('click', () => {
|
||||
galleryIndex = parseInt(dot.dataset.idx);
|
||||
updateGallery();
|
||||
galleryDots.querySelectorAll('.gallery-dot').forEach((d, i) =>
|
||||
d.classList.toggle('active', i === galleryIndex));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function syncDots() {
|
||||
const galleryDots = document.getElementById('gallery-dots');
|
||||
if (!galleryDots) return;
|
||||
galleryDots.querySelectorAll('.gallery-dot').forEach((d, i) =>
|
||||
d.classList.toggle('active', i === galleryIndex));
|
||||
}
|
||||
|
||||
function prevSlide() {
|
||||
galleryIndex = galleryIndex <= 0 ? getAllSlides().length - 1 : galleryIndex - 1;
|
||||
updateGallery();
|
||||
syncDots();
|
||||
}
|
||||
function nextSlide() {
|
||||
galleryIndex = galleryIndex >= getAllSlides().length - 1 ? 0 : galleryIndex + 1;
|
||||
updateGallery();
|
||||
syncDots();
|
||||
}
|
||||
|
||||
// Event bindings — uniquement sur la page réalisations
|
||||
if (filterTabs.length) {
|
||||
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();
|
||||
}));
|
||||
}
|
||||
|
||||
if (paginationContainer) {
|
||||
paginationContainer.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('button[data-page]');
|
||||
if (!btn) return;
|
||||
const page = Number(btn.getAttribute('data-page'));
|
||||
goToPage(page);
|
||||
});
|
||||
}
|
||||
|
||||
if (portfolioGrid) {
|
||||
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 && 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;
|
||||
|
||||
// ── Projets phares (bento grid) — 3 premiers projets du JSON ───────────────
|
||||
const featuredBento = document.getElementById('featured-bento-grid');
|
||||
if (featuredBento) {
|
||||
const featured = allProjects.slice(0, 3);
|
||||
const categoryLabels = { bot: 'Bot Discord', map: 'Fortnite Map', graphisme: 'Graphisme' };
|
||||
|
||||
featuredBento.innerHTML = featured.map((p, i) => {
|
||||
const isReverse = i % 2 === 1 ? ' reverse' : '';
|
||||
const statsHtml = (p.stats || []).slice(0, 4).map((s) => `
|
||||
<div class="bento-stat">
|
||||
<div class="bento-stat-icon"><i data-feather="${window.escapeHTML(s.icon || 'star')}"></i></div>
|
||||
<div class="bento-stat-value">${window.escapeHTML(s.value)}</div>
|
||||
<div class="bento-stat-label">${window.escapeHTML(s.label)}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
const featuresHtml = (p.features || []).slice(0, 6).map(f => `<span class="bento-feature">${window.escapeHTML(f)}</span>`).join('');
|
||||
const divider = i < featured.length - 1 ? '<div class="bento-divider"></div>' : '';
|
||||
|
||||
const prefix = isRoot ? '' : '../';
|
||||
const firstFile = Array.isArray(p.files) && p.files.length > 0 ? p.files[0] : null;
|
||||
const bentoCover = firstFile
|
||||
? `<img src="${window.escapeHTML(prefix + p.images + firstFile)}" alt="${window.escapeHTML(p.title)}" class="bento-cover-img" onerror="this.style.display='none'">`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="bento-project${isReverse} reveal" data-project-id="${window.escapeHTML(p.id)}" style="cursor:pointer;" role="button" tabindex="0" aria-label="Voir le projet ${window.escapeHTML(p.title)}">
|
||||
<div class="bento-image" style="background: ${window.escapeHTML(p.gradient)};">
|
||||
${bentoCover}
|
||||
<div class="bento-image-content">
|
||||
<span class="portfolio-tag">${window.escapeHTML(categoryLabels[p.category] || p.category)}</span>
|
||||
<h3>${window.escapeHTML(p.title)}</h3>
|
||||
<p>${window.escapeHTML(p.desc.substring(0, 100))}${p.desc.length > 100 ? '…' : ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bento-stats">${statsHtml}</div>
|
||||
<div class="bento-info">
|
||||
<h4>Description</h4>
|
||||
<p>${window.escapeHTML(p.desc)}</p>
|
||||
<div class="bento-features">${featuresHtml}</div>
|
||||
</div>
|
||||
</div>
|
||||
${divider}
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Replace feather icons
|
||||
if (window.feather && feather.replace) { try { feather.replace(); } catch(e) {} }
|
||||
|
||||
// Click to open modal
|
||||
featuredBento.querySelectorAll('.bento-project').forEach(el => {
|
||||
const open = () => openProjectModal(el.getAttribute('data-project-id'));
|
||||
el.addEventListener('click', open);
|
||||
el.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(); } });
|
||||
});
|
||||
|
||||
// Reveal animation
|
||||
setTimeout(() => {
|
||||
featuredBento.querySelectorAll('.reveal').forEach(el => el.classList.add('active'));
|
||||
}, 80);
|
||||
}
|
||||
|
||||
// Initial render
|
||||
renderProjects();
|
||||
renderPagination();
|
||||
});
|
||||
32
assets/js/pages/services-process.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// 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 * 1000);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', animateTimeline);
|
||||
window.addEventListener('load', animateTimeline);
|
||||
|
||||
|
||||
|
||||
});
|
||||
})();
|
||||