correction de bug formulaire de contact

This commit is contained in:
KIRITO_KMA 2026-03-16 12:26:36 +01:00
parent c14f0531ff
commit d705c47fbc
8 changed files with 379 additions and 665 deletions

View file

@ -2649,7 +2649,7 @@ footer {
transform: translateY(-50%);
z-index: 1;
width: 0%;
transition: width 1s ease;
transition: width 5s ease;
}
.process-timeline.animated::after {

163
assets/data/services.json Normal file
View file

@ -0,0 +1,163 @@
[
{
"id": "bots",
"nav": {
"label": "Bots Discord",
"icon": "code",
"stamp": null
},
"visual": {
"accentFrom": "#8b3a62",
"accentTo": "#a64d79",
"stamp": null
},
"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" }
]
},
{
"id": "maps",
"nav": {
"label": "Maps Fortnite",
"icon": "map",
"stamp": { "type": "new", "text": "New" }
},
"visual": {
"accentFrom": "#1a5e8b",
"accentTo": "#3a8bb5",
"stamp": { "type": "new", "text": "✦ New" }
},
"title": "Maps Fortnite 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" }
]
},
{
"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" }
]
},
{
"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 & Accompagnement",
"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" }
]
},
{
"id": "pack",
"nav": {
"label": "Pack Complet",
"icon": "package",
"stamp": { "type": "popular", "text": "Populaire" }
},
"visual": {
"accentFrom": "#8b3a8b",
"accentTo": "#b55ab5",
"stamp": { "type": "popular", "text": "★ Populaire", "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 Fortnite", "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" }
]
}
]

View 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);
});
})();

View file

@ -1,57 +1,164 @@
// Services page scripts (moved from inline in pages/services.html)
// Depends on ../main.js which initializes feather and other globals
/**
* OmégaKube — Services Tabs Renderer
* Charge assets/data/services.json et construit dynamiquement
* les onglets de navigation et leurs contenus.
*
* Dépend de main.js et services-process.js (déjà chargés avant ce script).
*/
(function() {
document.addEventListener('DOMContentLoaded', () => {
// Timeline Animation
const timeline = document.getElementById('process-timeline');
const steps = document.querySelectorAll('.process-step');
(function () {
'use strict';
function animateTimeline() {
if (!timeline) return;
const timelineTop = timeline.getBoundingClientRect().top;
/* ── Helpers ─────────────────────────────────────────────── */
if (timelineTop < window.innerHeight - 200) {
timeline.classList.add('animated');
// Animate steps sequentially
steps.forEach((step, index) => {
setTimeout(() => {
step.classList.add('active');
}, index * 400);
});
}
function stampHtml(stamp, extraClass) {
if (!stamp) return '';
const animated = stamp.animated ? ' service-stamp-animated' : '';
const extra = extraClass ? ' ' + extraClass : '';
return `<span class="service-stamp service-stamp-${stamp.type}${animated}${extra}">${stamp.text}</span>`;
}
window.addEventListener('scroll', animateTimeline);
window.addEventListener('load', animateTimeline);
// Service Tabs
const tabBtns = document.querySelectorAll('.service-tab-btn');
const tabContents = document.querySelectorAll('.service-tab-content');
if (tabBtns.length && tabContents.length) {
tabBtns.forEach(btn => {
btn.addEventListener('click', () => {
const tabId = btn.dataset.tab;
// Update buttons
tabBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// Update content
tabContents.forEach(content => {
content.classList.remove('active');
if (content.id === 'tab-' + tabId) {
content.classList.add('active');
}
});
// Re-init feather icons for new content (safe if feather is loaded)
if (window.feather) window.feather.replace();
});
});
function particlesHtml() {
return `<div class="service-visual-particles">${'<div class="particle"></div>'.repeat(6)}</div>`;
}
});
/* ── Rendu ───────────────────────────────────────────────── */
function renderNavBtn(service, isFirst) {
return `
<button class="service-tab-btn${isFirst ? ' active' : ''}" data-tab="${service.id}">
<i data-feather="${service.nav.icon}"></i>
<span>${service.nav.label}</span>
${stampHtml(service.nav.stamp)}
</button>`;
}
function renderVisual(service) {
const { accentFrom, accentTo, stamp } = service.visual;
return `
<div class="service-visual" style="--accent-primary:${accentFrom};--accent-secondary:${accentTo};">
${stampHtml(stamp, 'service-visual-stamp')}
${particlesHtml()}
<div class="service-visual-icon">
<div class="icon-main" style="background:linear-gradient(135deg,${accentFrom},${accentTo});">
<i data-feather="${service.nav.icon}"></i>
</div>
</div>
</div>`;
}
function renderHighlights(highlights) {
return highlights.map(h => `
<div class="service-highlight">
<div class="service-highlight-value">${h.value}</div>
<div class="service-highlight-label">${h.label}</div>
</div>`).join('');
}
function renderFeatures(features, accentFrom, accentTo) {
return features.map(f => `
<div class="service-feature-item">
<div class="service-feature-icon" style="background:linear-gradient(135deg,${accentFrom},${accentTo});">
<i data-feather="${f.icon}"></i>
</div>
<div class="service-feature-text">
<h4>${f.title}</h4>
<p>${f.desc}</p>
</div>
</div>`).join('');
}
function renderCtas(ctas) {
return ctas.map(c => `
<a href="${c.href}" class="btn btn-${c.style}">
<i data-feather="${c.icon}"></i>
${c.label}
</a>`).join('');
}
function renderTabContent(service, isFirst) {
const { accentFrom, accentTo } = service.visual;
const titleStamp = service.titleStamp
? ' ' + stampHtml({ ...service.titleStamp }, 'service-stamp-inline')
: '';
return `
<div class="service-tab-content${isFirst ? ' active' : ''}" id="tab-${service.id}">
<div class="service-tab-grid">
${renderVisual(service)}
<div class="service-tab-details">
<div class="service-tab-header">
<h2>${service.title}${titleStamp}</h2>
<p>${service.description}</p>
</div>
<div class="service-highlights">
${renderHighlights(service.highlights)}
</div>
<div class="service-tab-features">
${renderFeatures(service.features, accentFrom, accentTo)}
</div>
<div class="service-tab-cta">
${renderCtas(service.cta)}
</div>
</div>
</div>
</div>`;
}
/* ── Logique des onglets ─────────────────────────────────── */
function initTabs() {
const tabBtns = document.querySelectorAll('.service-tab-btn');
const tabContents = document.querySelectorAll('.service-tab-content');
if (!tabBtns.length || !tabContents.length) return;
tabBtns.forEach(btn => {
btn.addEventListener('click', () => {
const tabId = btn.dataset.tab;
tabBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
tabContents.forEach(content => {
content.classList.remove('active');
if (content.id === 'tab-' + tabId) content.classList.add('active');
});
if (window.feather) window.feather.replace();
});
});
}
/* ── Init ────────────────────────────────────────────────── */
async function init() {
const navContainer = document.getElementById('service-tabs-nav');
const contentContainer = document.getElementById('service-tabs-contents');
if (!navContainer || !contentContainer) return;
let services;
try {
const res = await fetch('../assets/data/services.json');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
services = await res.json();
} catch (err) {
console.error('[services-tabs.js] Impossible de charger services.json :', err);
return;
}
if (!Array.isArray(services) || services.length === 0) {
console.warn('[services-tabs.js] services.json est vide ou mal formé.');
return;
}
navContainer.innerHTML = services.map((s, i) => renderNavBtn(s, i === 0)).join('');
contentContainer.innerHTML = services.map((s, i) => renderTabContent(s, i === 0)).join('');
if (window.feather) window.feather.replace();
initTabs();
}
document.addEventListener('DOMContentLoaded', init);
})();

View file

@ -15,7 +15,7 @@ const ALLOWED_ORIGIN = '*'; // ou 'https://omegakube.com
/* ── Anti-spam : limite par IP (optionnel, nécessite accès fichier) ── */
const RATE_LIMIT_ENABLED = true;
const RATE_LIMIT_MAX = 30; // max envois
const RATE_LIMIT_MAX = 3; // max envois
const RATE_LIMIT_WINDOW = 3600; // par heure (secondes)
const RATE_LIMIT_DIR = __DIR__ . '/.rate_limits/';
@ -180,8 +180,8 @@ $emailBodyHtml = '<!DOCTYPE html>
/* Boundary pour email multipart */
$boundary = '----=_Part_' . md5(uniqid('', true));
$headers = "From: =?UTF-8?B?" . base64_encode($name) . "?= <{$email}>\r\n";
$headers .= "Reply-To: {$email}\r\n";
$headers = "From: " . RECIPIENT_NAME . " <" . RECIPIENT_EMAIL . ">\r\n";
$headers .= "Reply-To: =?UTF-8?B?" . base64_encode($name) . "?= <{$email}>\r\n";
$headers .= "To: " . RECIPIENT_NAME . " <" . RECIPIENT_EMAIL . ">\r\n";
$headers .= "MIME-Version: 1.0\r\n";
$headers .= "Content-Type: multipart/alternative; boundary=\"{$boundary}\"\r\n";
@ -226,4 +226,4 @@ if ($sent) {
} else {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Une erreur est survenue lors de l\'envoi. Veuillez réessayer ou nous contacter directement par Discord.']);
}
}