443 lines
15 KiB
JavaScript
443 lines
15 KiB
JavaScript
// Application JavaScript pour MRM & Co
|
|
// Génération dynamique des sections depuis config.js
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
// Initialisation
|
|
initializeColors();
|
|
renderServices();
|
|
renderTeam();
|
|
updateContactInfo();
|
|
setCurrentYear();
|
|
initializeMobileMenu();
|
|
initializeContactForm();
|
|
initializeSmoothScroll();
|
|
initializeHeaderScroll();
|
|
initializeHeroParallax();
|
|
initializeTestimonials();
|
|
});
|
|
|
|
/**
|
|
* Applique les couleurs personnalisées depuis la configuration
|
|
*/
|
|
function initializeColors() {
|
|
if (typeof config !== 'undefined' && config.colors) {
|
|
document.documentElement.style.setProperty('--color-primary', config.colors.primary);
|
|
document.documentElement.style.setProperty('--color-secondary', config.colors.secondary);
|
|
document.documentElement.style.setProperty('--color-accent', config.colors.accent);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Génère dynamiquement la section Services
|
|
*/
|
|
function renderServices() {
|
|
const servicesContainer = document.getElementById('services-container');
|
|
|
|
if (!servicesContainer || typeof config === 'undefined' || !config.services) {
|
|
console.error('Configuration des services non trouvée');
|
|
return;
|
|
}
|
|
|
|
servicesContainer.innerHTML = config.services.map((service, idx) => {
|
|
var iconId = 'icon-' + (service.icon || 'web');
|
|
return `
|
|
<div class="card-hover bg-white p-8 rounded-xl shadow-lg reveal" style="transition-delay:${idx * 60}ms">
|
|
<div class="mb-4 text-4xl text-indigo-600">
|
|
<svg class="service-icon w-12 h-12 text-primary" aria-hidden="true"><use href="#${iconId}"></use></svg>
|
|
</div>
|
|
<h3 class="text-xl font-bold text-gray-900 mb-3">${service.title}</h3>
|
|
<p class="text-gray-600 leading-relaxed">${service.description}</p>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
/**
|
|
* Génère dynamiquement la section Équipe
|
|
*/
|
|
function renderTeam() {
|
|
const teamContainer = document.getElementById('team-container');
|
|
|
|
if (!teamContainer || typeof config === 'undefined' || !config.team) {
|
|
console.error('Configuration de l\'équipe non trouvée');
|
|
return;
|
|
}
|
|
|
|
teamContainer.innerHTML = config.team.map(function (member, idx) {
|
|
// fallback avatar via ui-avatars (utilisé si l'image externe échoue)
|
|
var primaryColor = (config.colors && config.colors.primary) ? config.colors.primary.replace('#', '') : '2563eb';
|
|
var avatarFallback = 'https://ui-avatars.com/api/?name=' + encodeURIComponent(member.name) + '&background=ffffff&color=' + primaryColor + '&size=512';
|
|
|
|
return `
|
|
<div class="card-hover bg-white rounded-xl shadow-lg overflow-hidden">
|
|
<div class="aspect-square overflow-hidden bg-gray-100">
|
|
<img src="${member.image}"
|
|
alt="${member.name}"
|
|
loading="lazy"
|
|
data-fallback="${avatarFallback}"
|
|
class="w-full h-full object-cover transition-transform duration-300 hover:scale-110">
|
|
</div>
|
|
<div class="p-6">
|
|
<h3 class="text-xl font-bold text-gray-900 mb-1">${member.name}</h3>
|
|
<p class="text-sm font-semibold mb-3" style="color: var(--color-primary)">${member.role}</p>
|
|
<p class="text-gray-600 leading-relaxed mb-4">${member.description}</p>
|
|
<div class="flex space-x-3">
|
|
<a href="${member.linkedin}"
|
|
class="text-gray-400 hover:text-blue-600 transition text-xl"
|
|
aria-label="LinkedIn de ${member.name}">
|
|
🔗
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).map((html, i) => html.replace('card-hover', 'card-hover reveal')).join('');
|
|
|
|
// Attacher les handlers d'erreur d'image après insertion pour éviter les attributs inline
|
|
attachImageFallbacks();
|
|
}
|
|
|
|
function attachImageFallbacks() {
|
|
document.querySelectorAll('#team-container img[data-fallback]').forEach(function (img) {
|
|
if (img.__fallbackAttached) return;
|
|
img.addEventListener('error', function () {
|
|
var fb = img.getAttribute('data-fallback');
|
|
if (fb) img.src = fb;
|
|
});
|
|
img.__fallbackAttached = true;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Met à jour les informations de contact depuis la configuration
|
|
*/
|
|
function updateContactInfo() {
|
|
if (typeof config === 'undefined' || !config.company) {
|
|
return;
|
|
}
|
|
|
|
// Mise à jour du hero
|
|
const heroDesc = document.getElementById('hero-description');
|
|
if (heroDesc && config.company.description) {
|
|
heroDesc.textContent = config.company.description;
|
|
}
|
|
|
|
// Mise à jour des coordonnées
|
|
const emailEl = document.getElementById('contact-email');
|
|
if (emailEl && config.company.email) {
|
|
emailEl.textContent = config.company.email;
|
|
}
|
|
|
|
const addressEl = document.getElementById('contact-address');
|
|
if (addressEl && config.company.address) {
|
|
addressEl.textContent = config.company.address;
|
|
}
|
|
|
|
// Mise à jour du footer
|
|
const footerDesc = document.getElementById('footer-description');
|
|
if (footerDesc && config.company.tagline) {
|
|
footerDesc.textContent = config.company.tagline;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialise le menu mobile responsive
|
|
*/
|
|
function initializeMobileMenu() {
|
|
const mobileMenuBtn = document.getElementById('mobile-menu-btn');
|
|
const mobileMenu = document.getElementById('mobile-menu');
|
|
|
|
if (mobileMenuBtn && mobileMenu) {
|
|
mobileMenuBtn.addEventListener('click', function () {
|
|
mobileMenu.classList.toggle('hidden');
|
|
});
|
|
|
|
// Fermer le menu lors du clic sur un lien
|
|
const mobileLinks = mobileMenu.querySelectorAll('a');
|
|
mobileLinks.forEach(link => {
|
|
link.addEventListener('click', function () {
|
|
mobileMenu.classList.add('hidden');
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gestion du formulaire de contact
|
|
*/
|
|
function initializeContactForm() {
|
|
const contactForm = document.getElementById('contact-form');
|
|
|
|
if (contactForm) {
|
|
contactForm.addEventListener('submit', function (e) {
|
|
e.preventDefault();
|
|
|
|
// Récupération des données du formulaire
|
|
const formData = {
|
|
name: document.getElementById('name').value,
|
|
email: document.getElementById('email').value,
|
|
message: document.getElementById('message').value
|
|
};
|
|
|
|
// Simulation d'envoi (à remplacer par un vrai backend)
|
|
console.log('Formulaire soumis:', formData);
|
|
|
|
// Message de confirmation
|
|
alert('Merci pour votre message ! Nous vous répondrons dans les plus brefs délais.');
|
|
|
|
// Réinitialisation du formulaire
|
|
contactForm.reset();
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Scroll fluide pour les ancres de navigation
|
|
*/
|
|
function initializeSmoothScroll() {
|
|
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
|
anchor.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
const targetId = this.getAttribute('href');
|
|
|
|
if (targetId === '#') return;
|
|
|
|
const targetElement = document.querySelector(targetId);
|
|
if (targetElement) {
|
|
const headerOffset = 80; // Hauteur du header fixe
|
|
const elementPosition = targetElement.getBoundingClientRect().top;
|
|
const offsetPosition = elementPosition + window.pageYOffset - headerOffset;
|
|
|
|
window.scrollTo({
|
|
top: offsetPosition,
|
|
behavior: 'smooth'
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Animation au scroll pour les éléments
|
|
*/
|
|
function initializeScrollAnimations() {
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
entry.target.classList.add('animate-fade-in');
|
|
observer.unobserve(entry.target);
|
|
}
|
|
});
|
|
}, {
|
|
threshold: 0.12
|
|
});
|
|
|
|
// Observer tous les éléments avec la classe reveal
|
|
document.querySelectorAll('.reveal').forEach(el => {
|
|
observer.observe(el);
|
|
});
|
|
}
|
|
|
|
// Initialiser les animations au scroll après le chargement
|
|
window.addEventListener('load', initializeScrollAnimations);
|
|
|
|
/**
|
|
* Insère l'année courante dans le footer
|
|
*/
|
|
function setCurrentYear() {
|
|
var el = document.getElementById('current-year');
|
|
if (el) {
|
|
el.textContent = new Date().getFullYear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Change l'apparence du header quand on scroll
|
|
*/
|
|
function initializeHeaderScroll() {
|
|
var header = document.querySelector('header');
|
|
if (!header) return;
|
|
|
|
function onScroll() {
|
|
if (window.scrollY > 18) header.classList.add('header-scrolled');
|
|
else header.classList.remove('header-scrolled');
|
|
}
|
|
|
|
window.addEventListener('scroll', onScroll, { passive: true });
|
|
onScroll();
|
|
}
|
|
|
|
/**
|
|
* Micro-parallax for hero illustration
|
|
*/
|
|
function initializeHeroParallax() {
|
|
var hero = document.getElementById('hero-illustration');
|
|
if (!hero) return;
|
|
|
|
hero.addEventListener('mousemove', function (e) {
|
|
var rect = hero.getBoundingClientRect();
|
|
var x = (e.clientX - rect.left) / rect.width - 0.5; // -0.5..0.5
|
|
var y = (e.clientY - rect.top) / rect.height - 0.5;
|
|
var blobs = hero.querySelectorAll('.bg-blob');
|
|
blobs.forEach(function (b, i) {
|
|
var depth = (i === 0) ? 18 : -12;
|
|
b.style.transform = 'translate(' + (-x * depth) + 'px,' + (-y * depth) + 'px)';
|
|
});
|
|
});
|
|
|
|
// reset on leave
|
|
hero.addEventListener('mouseleave', function () {
|
|
var blobs = hero.querySelectorAll('.bg-blob');
|
|
blobs.forEach(function (b) { b.style.transform = ''; });
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Testimonials slider simple
|
|
*/
|
|
function initializeTestimonials() {
|
|
var slides = Array.from(document.querySelectorAll('#testimonial-slides .testimonial-slide'));
|
|
if (!slides.length) return;
|
|
var dotsContainer = document.getElementById('testimonial-dots');
|
|
var current = 0;
|
|
var autoplay = true;
|
|
var timer = null;
|
|
|
|
function goTo(idx) {
|
|
slides.forEach((s, i) => s.classList.toggle('active', i === idx));
|
|
Array.from(dotsContainer.children).forEach((d, i) => d.classList.toggle('active', i === idx));
|
|
current = idx;
|
|
}
|
|
|
|
function next() { goTo((current + 1) % slides.length); }
|
|
function prev() { goTo((current - 1 + slides.length) % slides.length); }
|
|
|
|
// dots
|
|
slides.forEach((s, i) => {
|
|
var dot = document.createElement('div');
|
|
dot.className = 'dot' + (i === 0 ? ' active' : '');
|
|
dot.addEventListener('click', function () { goTo(i); resetTimer(); });
|
|
dotsContainer.appendChild(dot);
|
|
});
|
|
|
|
document.getElementById('next-testimonial').addEventListener('click', function () { next(); resetTimer(); });
|
|
document.getElementById('prev-testimonial').addEventListener('click', function () { prev(); resetTimer(); });
|
|
|
|
function startTimer() { if (timer) clearInterval(timer); timer = setInterval(next, 5000); }
|
|
function resetTimer() { if (autoplay) startTimer(); }
|
|
|
|
// pause on hover
|
|
var container = document.querySelector('.testimonials');
|
|
container.addEventListener('mouseenter', function () { autoplay = false; if (timer) clearInterval(timer); });
|
|
container.addEventListener('mouseleave', function () { autoplay = true; startTimer(); });
|
|
|
|
// start
|
|
goTo(0);
|
|
startTimer();
|
|
}
|
|
|
|
/**
|
|
* Initialisation du canvas d'arrière-plan interactif
|
|
*/
|
|
function initializeBackgroundCanvas() {
|
|
if (typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
|
var canvas = document.getElementById('bg-canvas');
|
|
if (!canvas) return;
|
|
var ctx = canvas.getContext('2d');
|
|
var DPR = window.devicePixelRatio || 1;
|
|
var width, height;
|
|
var mouse = { x: -9999, y: -9999 };
|
|
|
|
function resize() {
|
|
width = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
|
|
height = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
|
|
canvas.width = Math.floor(width * DPR);
|
|
canvas.height = Math.floor(height * DPR);
|
|
canvas.style.width = width + 'px';
|
|
canvas.style.height = height + 'px';
|
|
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
|
|
}
|
|
|
|
window.addEventListener('resize', resize);
|
|
resize();
|
|
|
|
canvas.addEventListener('mousemove', function (e) {
|
|
mouse.x = e.clientX;
|
|
mouse.y = e.clientY;
|
|
});
|
|
canvas.addEventListener('mouseleave', function () { mouse.x = -9999; mouse.y = -9999; });
|
|
|
|
// particles/blobs
|
|
var blobs = [];
|
|
var count = Math.max(6, Math.min(28, Math.floor((width * height) / 200000)));
|
|
for (var i = 0; i < count; i++) {
|
|
blobs.push({
|
|
x: Math.random() * width,
|
|
y: Math.random() * height,
|
|
r: 80 + Math.random() * 260,
|
|
vx: (Math.random() - 0.5) * 0.2,
|
|
vy: (Math.random() - 0.5) * 0.2,
|
|
hue: 210 + Math.random() * 60
|
|
});
|
|
}
|
|
|
|
function drawBlob(b) {
|
|
var grd = ctx.createRadialGradient(b.x, b.y, b.r * 0.15, b.x, b.y, b.r);
|
|
var h = b.hue;
|
|
grd.addColorStop(0, 'hsla(' + h + ',90%,70%,0.20)');
|
|
grd.addColorStop(0.4, 'hsla(' + (h + 30) + ',80%,60%,0.12)');
|
|
grd.addColorStop(1, 'hsla(' + (h + 60) + ',70%,50%,0.02)');
|
|
ctx.fillStyle = grd;
|
|
ctx.beginPath();
|
|
ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
|
|
function step() {
|
|
ctx.clearRect(0, 0, width, height);
|
|
// subtle background overlay
|
|
ctx.globalCompositeOperation = 'source-over';
|
|
for (var i = 0; i < blobs.length; i++) {
|
|
var b = blobs[i];
|
|
// move
|
|
b.x += b.vx;
|
|
b.y += b.vy;
|
|
// slight attraction to mouse
|
|
if (mouse.x > -9998) {
|
|
var dx = mouse.x - b.x;
|
|
var dy = mouse.y - b.y;
|
|
var dist = Math.sqrt(dx * dx + dy * dy) + 1;
|
|
var force = Math.min(80 / dist, 0.9);
|
|
b.vx += dx * 0.0008 * force;
|
|
b.vy += dy * 0.0008 * force;
|
|
}
|
|
// slow down velocity
|
|
b.vx *= 0.995;
|
|
b.vy *= 0.995;
|
|
// wrap
|
|
if (b.x < -b.r) b.x = width + b.r;
|
|
if (b.x > width + b.r) b.x = -b.r;
|
|
if (b.y < -b.r) b.y = height + b.r;
|
|
if (b.y > height + b.r) b.y = -b.r;
|
|
|
|
drawBlob(b);
|
|
}
|
|
|
|
// small radial highlight near mouse for interactivity
|
|
if (mouse.x > -9998) {
|
|
var radial = ctx.createRadialGradient(mouse.x, mouse.y, 0, mouse.x, mouse.y, 180);
|
|
radial.addColorStop(0, 'rgba(255,255,255,0.06)');
|
|
radial.addColorStop(1, 'rgba(255,255,255,0)');
|
|
ctx.fillStyle = radial;
|
|
ctx.fillRect(mouse.x - 180, mouse.y - 180, 360, 360);
|
|
}
|
|
|
|
requestAnimationFrame(step);
|
|
}
|
|
|
|
step();
|
|
}
|
|
|
|
// lancer le background après DOM ready
|
|
window.addEventListener('load', function () { initializeBackgroundCanvas(); });
|