Faltbare 2-in-1-Buchhülle

-10% avec : SOLDESLIZIA10
23,95 € 24,95 €
Quantité
⭐ +100 000 zufriedene Leser
Housse Lizia
⭐ +100 000 zufriedene Leser

Housse Lizia

4.6/5 (536 Bewertungen)

Lieferung in 3 Werktagen
Made In France
MADE IN FRANCE
Französische Qualität
2 Jahre Garantie
2 ANS
Garantie
15 Tage Geld-zurück-Garantie
15 TAGE
Zufrieden oder Geld zurück
Geliefert in 1 bis 3 Tagen
GELIEFERT
1-5 Tage

Wie funktioniert Lizia?

01

Optimaler Schutz

Die Lizia-Hülle schützt Ihre Leselampe vor Stößen, Kratzern und Staub und verlängert so ihre Lebensdauer.

02

Einfacher Transport

Nehmen Sie Ihre Lizia überall sicher mit, dank dieser eleganten und praktischen Hülle, perfekt für den Transport.

03

Elegantes Design

Hergestellt in Frankreich mit hochwertigen Materialien vereint die Lizia-Hülle Schutz und Ästhetik, um Ihr Lesezubehör zu bewahren.

Housse Lizia

Lesen war noch nie so bequem

Ohne Ermüdung, mit einer Hand

An alle Daumen angepasst, gewinnen Sie Flexibilität, wo immer Sie sind. Ohne Anstrengung genießen Sie Ihre Lektüre voll und ganz, auch im Dunkeln, ohne jemanden zu stören.

Lizia light off Lizia light on

DIE LIZIA-VERPFLICHTUNGEN

Erfunden in Rennes, ist Lizia eine französische Innovation, die entwickelt wurde, um dauerhaften Lesekomfort zu bieten, mit lokalen Partnern und verantwortungsvollen Materialien.

France

Hergestellt in Frankreich

Produktion und Montage im Westen

Unsere Nylonteile werden im Westen Frankreichs produziert und dann in der Bretagne montiert. Mit der Wahl von Lizia unterstützen Sie eine 100% französische Herstellung, kurze Wege und lokales Industrie-Know-how, von der Konzeption bis zum Versand aus Rennes.

ESAT

Montiert in ESAT

Lokale Partner

Lizia engagiert sich sozial, indem es die Montage seiner Produkte an ESAT-Partner (Einrichtungen und Dienste für Arbeitshilfe) in der Bretagne überträgt und so die berufliche Integration von Menschen mit Behinderung fördert.

Recyclable

Recycelbare Materialien

Technisches PA12, recycelbarer Karton

Wir verwenden technisches PA12 für seine Robustheit und Langlebigkeit sowie 100% recycelbare Kartonverpackungen. Unser Ökodesign-Ansatz zielt darauf ab, unsere Umweltauswirkungen zu minimieren.

Médaille

Mit Lépine-Medaille ausgezeichnet

Ausgezeichnete Innovation

Die Lizia-Innovation wurde mit einer Medaille beim prestigeträchtigen Concours Lépine anerkannt und ausgezeichnet, ein Zeichen für ihre Genialität und Qualität.

Breveté

International patentiert

Geschützte Innovation

Unsere einzigartige Technologie ist durch internationale Patente geschützt und gewährleistet die Exklusivität unserer Einhand-Leselösung.

Étudiants

Von 2 Studenten entwickelt

Junges innovatives Unternehmen

Lizia entstand aus der Leidenschaft und dem Unternehmergeist zweier Studenten, die entschlossen sind, den Alltag der Leser zu verbessern.

Hergestellt in Frankreich

Unsere Nylonteile werden im Westen Frankreichs produziert und dann in der Bretagne montiert. Mit der Wahl von Lizia unterstützen Sie eine 100% französische Herstellung, kurze Wege und lokales Industrie-Know-how, von der Konzeption bis zum Versand aus Rennes.

Von vielen Lesern übernommen

★★★★★
4.6 | 536 Bewertungen

    { // ============================================================ // DOC RAPIDE A/B TEST (token/cookie) // ============================================================ // - Le test "pack_lecteur_pricing" assigne A ou B et stocke le résultat // dans le cookie: lizia_ab_pack_lecteur_pricing (durée: 7 jours). // - Si ce cookie existe déjà, le variant est conservé (pas de re-tirage). // - Le variant actif est accessible via AB.pack_lecteur_pricing. // // Console one-liner (DevTools): // - Voir le token actif: document.cookie.match(/(?:^|;\s*)lizia_ab_pack_lecteur_pricing=([^;]+)/)?.[1] ?? null // - Forcer A: document.cookie="lizia_ab_pack_lecteur_pricing=A; path=/; max-age=604800; SameSite=Lax"; location.reload(); // - Forcer B: document.cookie="lizia_ab_pack_lecteur_pricing=B; path=/; max-age=604800; SameSite=Lax"; location.reload(); // - Forcer n'importe quel variant (A/B/C/...): document.cookie=`lizia_ab_pack_lecteur_pricing=${"C"}; path=/; max-age=604800; SameSite=Lax`; location.reload(); // - Reset (re-tirage aléatoire): document.cookie="lizia_ab_pack_lecteur_pricing=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"; location.reload(); // // ============================================================ // A/B TEST ENGINE // ============================================================ const AB_TESTS_CONFIG = { pack_lecteur_pricing: { weight: [50, 50], // 50% A / 50% B variants: ["A", "B"], }, }; const AB_COOKIE_PREFIX = "lizia_ab_"; const AB_COOKIE_DAYS = 7; function abSetCookie(name, value, days) { const expires = new Date(Date.now() + days * 864e5).toUTCString(); document.cookie = `${AB_COOKIE_PREFIX}${name}=${value}; expires=${expires}; path=/; SameSite=Lax`; } function abGetCookie(name) { const match = document.cookie.match( new RegExp("(?:^|; )" + AB_COOKIE_PREFIX + name + "=([^;]*)"), ); return match ? decodeURIComponent(match[1]) : null; } function abAssignVariant(testId) { const config = AB_TESTS_CONFIG[testId]; if (!config) return null; let variant = abGetCookie(testId); if (variant && config.variants.includes(variant)) return variant; let rand = Math.random() * 100; let cumulative = 0; for (let i = 0; i < config.variants.length; i++) { cumulative += config.weight[i]; if (rand < cumulative) { variant = config.variants[i]; break; } } variant = variant || config.variants[0]; abSetCookie(testId, variant, AB_COOKIE_DAYS); return variant; } // Helper analytics (GA4 via gtag + fallback dataLayer) function abTrack(eventName, params) { try { if (typeof gtag === "function") { gtag("event", eventName, params); } if (window.dataLayer) { window.dataLayer.push({ event: eventName, ...params }); } } catch (e) { // silencieux } } // ---- Résolution des variants actifs ---- const AB = { pack_lecteur_pricing: abAssignVariant("pack_lecteur_pricing"), }; // Contexte analytics pour segmenter par variant try { if (typeof gtag === "function") { gtag("set", "user_properties", { ab_pack_lecteur_pricing: AB.pack_lecteur_pricing, }); } if (typeof window.clarity === "function") { window.clarity("set", "ab_pack_lecteur_pricing", AB.pack_lecteur_pricing); } } catch (e) { // silencieux } // Exposition du test au chargement abTrack("ab_test_exposure", { test_id: "pack_lecteur_pricing", variant: AB.pack_lecteur_pricing, page: window.location.pathname, }); // ============================================================ // CONFIGURATION PACK COMBO (A/B PRIX) — cookie partagé page Lizia // ============================================================ // --- CONSTANTES ET DONNÉES --- const HOUSSE_PRICE = 23.95; const HOUSSE_ORIGINAL_PRICE = 24.95; const COMBO_PRODUCT_A = "16410772930909"; // 34.90€ avec réduction const COMBO_PRODUCT_B = "16595451609437"; // 39.90€ sans réduction const COMBO_PRODUCT_ID = AB.pack_lecteur_pricing === "B" ? COMBO_PRODUCT_B : COMBO_PRODUCT_A; const COMBO_FORM_INPUT_A = "personalize-input-combo"; const COMBO_FORM_INPUT_B = "personalize-input-combo-b"; const COMBO_PRICE = AB.pack_lecteur_pricing === "B" ? 39.9 : 34.9; // Prix barré toujours 44,90€ (→ 49,85€ si gravure) ; badge promo uniquement sur A const COMBO_ORIGINAL_PRICE = 44.9; const COMBO_BADGE = AB.pack_lecteur_pricing === "B" ? null : `-${Math.round(((44.9 - 34.9) / 44.9) * 100)}%`; const PERSONALIZATION_PRICE = 4.95; const PRODUCT_ID = "8910785347933"; const PACK_HOUSSE = "housse"; const PACK_COMBO = "housse-lizia"; // Mapping pack → ID produit Shopify (méthode Lizia) const packIDMapping = { [PACK_HOUSSE]: PRODUCT_ID, [PACK_COMBO]: COMBO_PRODUCT_ID, }; const PACKS = [ { id: PACK_HOUSSE, name: "Hülle", price: HOUSSE_PRICE, originalPrice: HOUSSE_ORIGINAL_PRICE, image: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/housse_vierge.webp?v=1784628866", description: "Die elegante und praktische Lizia-Schutzhülle.", features: [ "Schutz vor Stößen und Kratzern", "Einfacher Transport", "Hergestellt in Frankreich", "Weltkarten-Design", ], }, { id: PACK_COMBO, name: "Hülle + Lizia", price: COMBO_PRICE, originalPrice: COMBO_ORIGINAL_PRICE, badgeTop: "Top-Angebot", badge: COMBO_BADGE, image: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/POCHETTE-BLEU_LIZIA-VERT.png?v=1790546593", description: "Das Essential-Pack: die Lizia-Leselampe und ihre Schutzhülle.", features: [ "Lizia-Leselampe", "Schutzhülle", "Integriertes Lesezeichen", "Einhand-Lesen", "Optionale Gravur", ], }, ]; // Couleurs disponibles pour la housse avec leurs images const COLORS = [ { id: "bleu", name: "Weltkarte blauer Hintergrund", hex: "#5a8fc4", variantID: "55293012083037", available: true, thumbnailImage: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/housse_vierge.webp?v=1784628866", images: [ "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/housse_bleue_illu_1.png?v=1786704460", "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/housse_bleue_illu_2.png?v=1786704459", "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/housse_bleue_illu_3.png?v=1786704459", "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/housse_bleue_illu_4.png?v=1786704459", "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/housse_bleue_illu_5.png?v=1786704459", ], }, { id: "marron", name: "Weltkarte brauner Hintergrund", hex: "#8b6b4a", variantID: "55293015359837", available: false, thumbnailImage: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/housse_marron.jpg?v=1771518450", images: [], }, { id: "rose", name: "Weltkarte rosa Hintergrund", hex: "#f4a6d7", variantID: "63499160584541", available: true, thumbnailImage: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/housse_rose.jpg?v=1771518196", images: [ "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/housse_rose_illu_1.png?v=1786704458", "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/housse_rose_illu_2.png?v=1786704458", "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/housse_rose_illu_3.png?v=1786704458", "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/housse_rose_illu_4.png?v=1786704459", "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/housse_rose_illu_5.png?v=1786704456", ], }, ]; const LIZIA_COLORS = [ { id: "vert", hex: "#cde2c5" }, { id: "blanc", hex: "#f4f4f4", border: true }, { id: "noir", hex: "#212121" }, { id: "bleu", hex: "#7ebfcd" }, { id: "violet", hex: "#d1a1e9" }, ]; // Vignettes pack combo : housse × couleur Lizia const COMBO_PACK_IMAGES = { bleu: { blanc: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/POCHETTE-BLEU_LIZIA-BLANC.png?v=1790546593", bleu: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/POCHETTE-BLEU_LIZIA-BLEU.png?v=1790546594", violet: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/POCHETTE-BLEU_LIZIA-VIOLET.png?v=1790546593", noir: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/POCHETTE-BLEU_LIZIA-NOIR.png?v=1790546593", vert: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/POCHETTE-BLEU_LIZIA-VERT.png?v=1790546593", }, rose: { blanc: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/POCHETTE-ROSE_LIZIA-BLANC.png?v=1790546594", vert: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/POCHETTE-ROSE_LIZIA-VERT.png?v=1790546594", violet: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/POCHETTE-ROSE_LIZIA-VIOLET.png?v=1790546594", noir: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/POCHETTE-ROSE_LIZIA-NOIR.png?v=1790546594", bleu: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/POCHETTE-ROSE_LIZIA-BLEU.png?v=1790546594", }, }; const COMBO_GALLERY_IMAGE_1 = { bleu: { vert: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/image_1_-_Bleu_vert.png?v=1786704458", blanc: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/image_1_-_Bleu_blanc.png?v=1786704458", bleu: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/image_1_-_Bleu_bleu.png?v=1786704458", violet: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/image_1_-_Bleu_violet.png?v=1786704458", noir: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/image_1_-_Bleu_noir.png?v=1786704457", }, rose: { noir: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/image_1_-_Rose_noir.png?v=1786704458", vert: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/image_1_-_Rose_vert.png?v=1786704458", bleu: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/image_1_-_Rose_bleu.png?v=1786704459", violet: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/image_1_-_Rose_violet.png?v=1786704458", blanc: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/image_1_-_Rose_blanc.png?v=1786704458", }, }; const LIZIA_PRESENTATION_IMAGES = { vert: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/de_presentation_lizia_vert.webp?v=1763396480", blanc: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/de_presentation_lizia_blanc.webp?v=1763396480", noir: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/de_presentation_lizia_noir.webp?v=1763396479", bleu: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/de_presentation_lizia_bleu.webp?v=1763396480", violet: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/de_presentation_lizia_violet.webp?v=1763396480", }; const LIZIA_NIGHT_IMAGE = "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/de_presentation_lizia_nuit.webp?v=1763996161"; const HOUSSE_ILLU_2 = { bleu: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/housse_bleue_illu_2.png?v=1786704459", rose: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/housse_rose_illu_2.png?v=1786704458", }; const HOUSSE_ILLU_5 = { bleu: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/housse_bleue_illu_5.png?v=1786704459", rose: "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/housse_rose_illu_5.png?v=1786704456", }; function getComboGalleryImages(housseColor, liziaColor) { const housse = housseColor === "rose" ? "rose" : "bleu"; const lizia = liziaColor || "vert"; return [ COMBO_GALLERY_IMAGE_1[housse]?.[lizia] || COMBO_GALLERY_IMAGE_1.bleu.vert, LIZIA_PRESENTATION_IMAGES[lizia] || LIZIA_PRESENTATION_IMAGES.vert, LIZIA_NIGHT_IMAGE, HOUSSE_ILLU_2[housse] || HOUSSE_ILLU_2.bleu, HOUSSE_ILLU_5[housse] || HOUSSE_ILLU_5.bleu, ]; } function getComboPackVignette(housseColor, liziaColor) { const housse = housseColor === "rose" ? "rose" : "bleu"; const lizia = liziaColor || "vert"; return ( COMBO_PACK_IMAGES[housse]?.[lizia] || COMBO_PACK_IMAGES.bleu.vert ); } // Mapping des variants Shopify const variantIDs = { 8910785347933: { bleu: { normal: "55293012083037" }, marron: { normal: "55293015359837" }, rose: { normal: "63499160584541" }, }, [COMBO_PRODUCT_A]: { Bleu: { Blanc: { normal: "65556450148701", personnalise: "65556450115933" }, Noir: { normal: "65556450214237", personnalise: "65556450181469" }, Vert: { normal: "65556450279773", personnalise: "65556450247005" }, Bleu: { normal: "65556450345309", personnalise: "65556450312541" }, Violet: { normal: "65556450410845", personnalise: "65556450378077" }, }, Rose: { Blanc: { normal: "65556450476381", personnalise: "65556450443613" }, Noir: { normal: "65556450541917", personnalise: "65556450509149" }, Vert: { normal: "65556450607453", personnalise: "65556450574685" }, Bleu: { normal: "65556450672989", personnalise: "65556450640221" }, Violet: { normal: "65556450738525", personnalise: "65556450705757" }, }, }, // Pack combo B — 39.90€ (housse × lizia × perso) [COMBO_PRODUCT_B]: { Bleu: { Blanc: { normal: "66061886095709", personnalise: "66061886128477" }, Noir: { normal: "66061886161245", personnalise: "66061886194013" }, Vert: { normal: "66061886226781", personnalise: "66061886259549" }, Bleu: { normal: "66061886292317", personnalise: "66061886325085" }, Violet: { normal: "66061886357853", personnalise: "66061886390621" }, }, Rose: { Blanc: { normal: "66061886423389", personnalise: "66061886456157" }, Noir: { normal: "66061886488925", personnalise: "66061886521693" }, Vert: { normal: "66061886554461", personnalise: "66061886587229" }, Bleu: { normal: "66061886619997", personnalise: "66061886652765" }, Violet: { normal: "66061886685533", personnalise: "66061886718301" }, }, }, }; // Reviews data const reviewsData = [ { firstName: "Laura", lastName: "M", gender: "Leserin", rating: 5, date: { month: "Sept", year: 2025 }, title: "Eine geniale Entdeckung!", content: "Ich habe sie erhalten und es ist einfach zu cool!", verified: true, }, { firstName: "Nathalie", lastName: "M", gender: "Leserin", rating: 5, date: { month: "Sept", year: 2025 }, title: "Einfaches Lesen ohne andere zu stören, perfekt!", content: "Ich habe meine Lizia vor ein paar Tagen erhalten. Es ist Weihnachten im September, welch ein Glück, im Bett lesen zu können, ohne meinen Partner zu stören, in den Verkehrsmitteln...", verified: true, }, { firstName: "Audrey", lastName: "T", gender: "Leserin", rating: 5, date: { month: "Juli", year: 2025 }, title: "Praktisch und zuverlässig, ich nutze es schon lange", content: "Es ist großartig zum Lesen, ich habe es seit mehreren Monaten, es ist super!", verified: true, }, { firstName: "Sophie", lastName: "D", gender: "Leserin", rating: 5, date: { month: "Aug", year: 2025 }, title: "Unverzichtbar für meine Leseabende", content: "Ich kann nicht mehr ohne! Perfekt zum Lesen am Abend, ohne das Licht im Zimmer anzumachen.", verified: true, }, { firstName: "Marc", lastName: "L", gender: "Leser", rating: 5, date: { month: "Juni", year: 2025 }, title: "Perfektes Geschenk für Leser", content: "Meiner Frau geschenkt, sie liebt es! Die Qualität stimmt und das Design ist elegant.", verified: true, }, { firstName: "Émilie", lastName: "R", gender: "Leserin", rating: 5, date: { month: "Sept", year: 2025 }, title: "Lizia revolutioniert meine Lesezeiten", content: "Schluss mit müden Armen und Lichtproblemen! Ich empfehle es zu 100%.", verified: true, }, { firstName: "Thomas", lastName: "B", gender: "Leser", rating: 5, date: { month: "Juli", year: 2025 }, title: "Ausgezeichnetes Produkt, entspricht meinen Erwartungen", content: "Schnelle Lieferung, qualitativ hochwertiges Produkt. Ich lese jetzt überall ohne Einschränkungen.", verified: true, }, { firstName: "Charlotte", lastName: "V", gender: "Leserin", rating: 5, date: { month: "Aug", year: 2025 }, title: "Ein Muss für alle Leser", content: "Einfach, effektiv und so praktisch. Meine Kinder haben sogar ihre eigenen verlangt!", verified: true, }, { firstName: "Pierre", lastName: "G", gender: "Leser", rating: 5, date: { month: "Mai", year: 2025 }, title: "Französische Qualität auf höchstem Niveau", content: "Gerne unterstütze ich ein französisches Unternehmen. Das Produkt ist robust und gut durchdacht.", verified: true, }, { firstName: "Isabelle", lastName: "F", gender: "Leserin", rating: 5, date: { month: "Sept", year: 2025 }, title: "Perfekt zum Lesen auf Reisen", content: "Ich nehme es überall mit: Zug, Flugzeug, Hotel. Es ist zu meinem unverzichtbaren Accessoire geworden.", verified: true, }, { firstName: "Antoine", lastName: "H", gender: "Leser", rating: 5, date: { month: "Juni", year: 2025 }, title: "Komfortabel und diskret", content: "Die Lampe ist leistungsstark, stört aber niemanden. Ideal für nächtliche Lektüren.", verified: true, }, { firstName: "Camille", lastName: "P", gender: "Leserin", rating: 5, date: { month: "Aug", year: 2025 }, title: "Elegantes und funktionales Design", content: "Ich liebe das schlichte Design und die verfügbaren Farben. Außerdem ist es super praktisch!", verified: true, }, { firstName: "Julien", lastName: "M", gender: "Leser", rating: 5, date: { month: "Juli", year: 2025 }, title: "Ausgezeichnetes Preis-Leistungs-Verhältnis", content: "Für den Preis ist es wirklich eine ausgezeichnete Investition. Ich empfehle es ohne Zögern.", verified: true, }, { firstName: "Léa", lastName: "S", gender: "Leserin", rating: 5, date: { month: "Sept", year: 2025 }, title: "Meine Kinder lieben es auch", content: "Meine Kinder nutzen es jeden Abend für ihre Abendlektüre. Es ist zu einem Ritual geworden!", verified: true, }, { firstName: "Maxime", lastName: "C", gender: "Leser", rating: 5, date: { month: "Aug", year: 2025 }, title: "Innovation im Dienste des Lesens", content: "Endlich ein Produkt, das an die wahren Bedürfnisse der Leser denkt. Bravo für diese Innovation!", verified: true, }, ]; // Images par couleur/variante const PRODUCT_IMAGES_BY_VARIANT = { bleu: COLORS.find((c) => c.id === "bleu")?.images || [], marron: COLORS.find((c) => c.id === "marron")?.images || [], rose: COLORS.find((c) => c.id === "rose")?.images || [], }; // Suivi de l'état de lecture des vidéos (autoplay, pause utilisateur) const videoPlaybackStates = {}; // Fonction pour obtenir les images selon la variante function getProductImages() { if (state.pack === PACK_COMBO) { return getComboGalleryImages(state.color, state.colors[0] || "vert"); } const color = state.color || "bleu"; return ( PRODUCT_IMAGES_BY_VARIANT[color] || PRODUCT_IMAGES_BY_VARIANT["bleu"] ); } // --- ÉTAT DE L'APPLICATION --- let state = { pack: PACK_COMBO, color: "bleu", colors: ["vert"], personalization: [""], quantity: 1, currentImageIndex: 0, }; // Tracker la quantité précédente pour les animations let previousQuantity = state.quantity; // Cache d'images préchargées pour éviter le lag const imageCache = new Map(); // Cache spécifique pour les images de la galerie principale (préchargées et décodées) const galleryImageCache = new Map(); // Fonction de préchargement des images function preloadColorImages() { COLORS.forEach((color) => { if (color.available !== false) { if (color.thumbnailImage && !imageCache.has(color.thumbnailImage)) { const img = new Image(); img.src = color.thumbnailImage; imageCache.set(color.thumbnailImage, img); } color.images.forEach((url) => { if (!imageCache.has(url)) { const img = new Image(); img.src = url; imageCache.set(url, img); } }); } }); Object.values(COMBO_PACK_IMAGES).forEach((byLizia) => { Object.values(byLizia).forEach((url) => { if (!imageCache.has(url)) { const img = new Image(); img.src = url; imageCache.set(url, img); } }); }); Object.values(COMBO_GALLERY_IMAGE_1).forEach((byLizia) => { Object.values(byLizia).forEach((url) => { if (!imageCache.has(url)) { const img = new Image(); img.src = url; imageCache.set(url, img); } }); }); Object.values(LIZIA_PRESENTATION_IMAGES).forEach((url) => { if (!imageCache.has(url)) { const img = new Image(); img.src = url; imageCache.set(url, img); } }); [LIZIA_NIGHT_IMAGE, ...Object.values(HOUSSE_ILLU_2), ...Object.values(HOUSSE_ILLU_5)].forEach( (url) => { if (!imageCache.has(url)) { const img = new Image(); img.src = url; imageCache.set(url, img); } }, ); } // Précharger et décoder toutes les images de la galerie actuelle function preloadGalleryImages(imageUrls) { imageUrls.forEach((url) => { // Ignorer les vidéos (qui commencent par "VIDEO:") if (url.startsWith("VIDEO:")) { return; } if (!galleryImageCache.has(url)) { const img = new Image(); img.src = url; // Décoder l'image pour qu'elle soit prête à l'affichage img .decode() .then(() => { galleryImageCache.set(url, img); }) .catch((err) => { console.warn("Erreur de décodage de l'image:", url, err); // Mettre quand même en cache même si le décodage échoue galleryImageCache.set(url, img); }); } }); } // --- SÉLECTEURS DOM --- const mainImage = document.getElementById("mainProductImage"); const mainImageContainer = document.querySelector(".mainImageContainer"); const prevBtn = document.getElementById("prevImageBtn"); const nextBtn = document.getElementById("nextImageBtn"); const imageDotsContainer = document.getElementById("imageDots"); const thumbnailContainer = document.getElementById("thumbnailContainer"); const packSelectorContainer = document.getElementById("packSelector"); const colorSelectorContainer = document.getElementById("colorSelector"); const liziaItemsContainer = document.getElementById("liziaItemsContainer"); const colorsPersoLabel = document.getElementById("colorsPersoLabel"); const addToCartBtn = document.getElementById("addToCartBtn"); const addToCartBtnMobile = document.getElementById("addToCartBtnMobile"); // Sélecteurs pour la popup const packPopup = document.getElementById("packPopup"); const popupClose = document.getElementById("popupClose"); const popupImage = document.getElementById("popupImage"); const popupTitle = document.getElementById("popupTitle"); const popupDescription = document.getElementById("popupDescription"); const popupFeatures = document.getElementById("popupFeatures"); // --- FONCTIONS DE RENDU --- /** Ajouter les contrôles vidéo natifs */ function setupVideoControls(videoElement) { if (!videoElement) return; videoElement.setAttribute("controls", ""); } /** Rendu de la galerie d'images */ function renderImageGallery() { if (!mainImage || !mainImageContainer) return; const currentImages = getProductImages(); if (!currentImages || currentImages.length === 0) return; // Précharger et décoder toutes les images de la galerie pour un changement instantané preloadGalleryImages(currentImages); // Utiliser l'image du cache si disponible, sinon charger normalement const currentImageUrl = currentImages[state.currentImageIndex]; if (!currentImageUrl) return; const isVideo = currentImageUrl.startsWith("VIDEO:"); const actualUrl = isVideo ? currentImageUrl.replace("VIDEO:", "") : currentImageUrl; // Gérer les vidéos différemment des images if (isVideo) { // Masquer l'image if (mainImage) { mainImage.style.display = "none"; } // Vérifier si une vidéo existe déjà, sinon en créer une let videoElement = mainImageContainer.querySelector("video.mainImage"); if (!videoElement) { videoElement = document.createElement("video"); videoElement.className = "mainImage"; videoElement.setAttribute("playsinline", ""); videoElement.setAttribute("muted", ""); videoElement.setAttribute("autoplay", ""); videoElement.setAttribute("loop", ""); videoElement.setAttribute("controls", ""); videoElement.style.width = "100%"; videoElement.style.height = "100%"; videoElement.style.objectFit = "cover"; videoElement.style.position = "absolute"; videoElement.style.top = "0"; videoElement.style.left = "0"; videoElement.style.display = "block"; videoElement.style.cursor = "pointer"; videoElement.style.backgroundColor = "#000"; // Fond noir pour éviter le fond rose mainImageContainer.appendChild(videoElement); } else { videoElement.style.display = "block"; videoElement.style.cursor = "pointer"; } // Ajouter les contrôles vidéo (play/pause + barre de progression) setupVideoControls(videoElement); // Vérifier si la source existe et si l'URL a changé let source = videoElement.querySelector("source"); const currentSrc = source ? source.src : ""; if (!source || currentSrc !== actualUrl) { // Vider la vidéo et recréer la source videoElement.innerHTML = ""; source = document.createElement("source"); source.src = actualUrl; source.type = "video/mp4"; videoElement.appendChild(source); // Réinitialiser la vidéo à 0 et charger videoElement.currentTime = 0; videoElement.load(); videoElement.play().catch((err) => { console.warn("Erreur de lecture vidéo:", err); }); } else { // Même vidéo, mais toujours réinitialiser à 0 videoElement.currentTime = 0; if (videoElement.paused) { // Si la vidéo est déjà chargée mais en pause, la relancer videoElement.play().catch((err) => { console.warn("Erreur de lecture vidéo:", err); }); } } } else { // Masquer la vidéo si elle existe et afficher l'image const videoElement = mainImageContainer.querySelector("video"); if (videoElement) { videoElement.style.display = "none"; videoElement.pause(); } if (mainImage) { mainImage.style.display = "block"; const cachedImage = galleryImageCache.get(currentImageUrl); if (cachedImage && cachedImage.complete) { // L'image est déjà chargée et décodée, l'utiliser directement mainImage.src = actualUrl; } else { // Charger l'image normalement mainImage.src = actualUrl; } } } // Points de navigation if (imageDotsContainer) { imageDotsContainer.innerHTML = currentImages .map( (_, index) => ``, ) .join(""); } // Miniatures avec lazy loading if (thumbnailContainer) { thumbnailContainer.innerHTML = currentImages .map((img, index) => { const isVideoThumb = img.startsWith("VIDEO:"); const actualUrl = isVideoThumb ? img.replace("VIDEO:", "") : img; return ``; }) .join(""); } // Charger les images visibles loadVisibleImages(); // Réinitialiser l'observer pour les nouvelles images setTimeout(() => { const lazyImages = document.querySelectorAll(".lazy-image"); lazyImages.forEach((img) => imageObserver.observe(img)); }, 100); } /** Charger les images visibles (lazy loading optimisé) */ function loadVisibleImages() { const lazyImages = document.querySelectorAll(".lazy-image"); lazyImages.forEach((element) => { const rect = element.getBoundingClientRect(); const isVisible = rect.top < window.innerHeight + 200 && rect.bottom > -200; // Zone de chargement anticipé if (isVisible) { // Gérer les images if (element.tagName === "IMG" && element.dataset.src && !element.src) { // Charger directement l'image sans délai element.src = element.dataset.src; element.classList.add("loaded"); } // Gérer les vidéos dans les miniatures - seulement charger la première frame (preload="metadata") else if ( element.tagName === "VIDEO" && element.classList.contains("videoPreview") ) { // Ne rien faire - la vidéo dans les miniatures utilise preload="metadata" // pour charger seulement la première frame, mais ne se lance pas automatiquement } } }); } /** Image pack selon le pack et les couleurs sélectionnées */ function getPackImage(packId) { const id = packId || state.pack; if (id === PACK_COMBO) { return getComboPackVignette(state.color, state.colors[0] || "vert"); } const color = COLORS.find((c) => c.id === state.color); return ( color?.thumbnailImage || PACKS.find((p) => p.id === PACK_HOUSSE)?.image ); } function getColorCheckmarkSvg(colorId) { const fill = colorId === "rose" || colorId === "blanc" || colorId === "vert" ? "#000" : "#fff"; return ` `; } function getHousseColorThumbsHtml() { const availableColors = COLORS.filter((color) => color.available !== false); return availableColors .map( (color) => ` `, ) .join(""); } /** Rendu du sélecteur de packs */ function renderPackSelector() { if (!packSelectorContainer) return; packSelectorContainer.innerHTML = PACKS.map((pack) => { const isSelected = state.pack === pack.id; const packImage = getPackImage(pack.id); const topBadge = pack.badgeTop || null; return `
    ${ topBadge ? `` : "" }
    `; }).join(""); } /** Rendu du sélecteur de couleur housse (vignettes) */ function renderColorSelector() { if (!colorSelectorContainer) return; const thumbs = getHousseColorThumbsHtml(); const isCombo = state.pack === PACK_COMBO; if (isCombo) { colorSelectorContainer.innerHTML = `
    Hülle
    ${thumbs}
    Hülle
    ${thumbs}
    `; } else { colorSelectorContainer.innerHTML = `
    ${thumbs}
    `; } } /** Calcul de la réduction totale (Pochette n'a pas de réduction) */ function calculateTotalDiscount() { // Pochette n'a aucune réduction, prix fixe à 4.95€ par unité return { packDiscount: 0, quantityDiscount: 0, total: 0, }; } /** Rendu du sélecteur de quantité avec système à 3 bulles */ function renderQuantitySelector() { if (!quantitySelectorContainer) return; const qty = state.quantity; // Déterminer les bulles à afficher let leftBubble, middleBubble, rightBubble; let leftSelected = false; let middleSelected = false; if (qty === 1) { leftBubble = { type: "number", value: 1, action: "set" }; middleBubble = { type: "number", value: 2, action: "set" }; rightBubble = { type: "increment", value: "+", action: "increment" }; leftSelected = true; middleSelected = false; } else if (qty === 2) { leftBubble = { type: "number", value: 1, action: "set" }; middleBubble = { type: "number", value: 2, action: "set" }; rightBubble = { type: "increment", value: "+", action: "increment" }; leftSelected = false; middleSelected = true; } else { leftBubble = { type: "decrement", value: "-", action: "decrement" }; middleBubble = { type: "number", value: qty, action: "set" }; rightBubble = { type: "increment", value: "+", action: "increment" }; leftSelected = false; middleSelected = true; } quantitySelectorContainer.innerHTML = `
    `; } /** Rendu de la jauge de réduction */ function renderDiscountGauge() { if (!discountGaugeContainer) return; const discount = calculateTotalDiscount(); const discountValue = discount.total; const maxDiscount = 15; let fillPercentage = (discountValue / maxDiscount) * 101; fillPercentage = Math.min(Math.max(fillPercentage, 0), 101); let progressBar = discountGaugeContainer.querySelector( ".discountProgressBar", ); let progressFill = discountGaugeContainer.querySelector( ".discountProgressFill", ); if (!progressBar || !progressFill) { discountGaugeContainer.innerHTML = `
    0%
    0%
    `; progressBar = discountGaugeContainer.querySelector( ".discountProgressBar", ); progressFill = discountGaugeContainer.querySelector( ".discountProgressFill", ); } if (progressFill) { progressFill.style.width = `${fillPercentage}%`; } // Mettre à jour les labels de réduction updateDiscountLabels(discountValue); } function updateDiscountLabels(currentDiscount) { const discountValues = [0, 5, 10, 15]; let closestDiscount = discountValues[0]; let minDiff = Math.abs(currentDiscount - closestDiscount); discountValues.forEach((val) => { const diff = Math.abs(currentDiscount - val); if (diff < minDiff) { minDiff = diff; closestDiscount = val; } }); const progressBar = document.querySelector(".discountProgressBar"); const fillLabel = document.querySelector(".discountLabel--fill"); const baseLabel = document.querySelector(".discountLabel--base"); const isZero = closestDiscount === 0; if (progressBar) { progressBar.classList.toggle("zero", isZero); } if (fillLabel) { fillLabel.textContent = isZero ? "0%" : `-${closestDiscount.toString()}%`; } if (baseLabel) { baseLabel.textContent = "0%"; } } /** Animer le pourcentage avec un compteur */ function animatePercentage(element, targetValue) { const currentText = element.textContent; const currentValue = parseInt(currentText.replace(/[^0-9]/g, "")) || 0; if (currentValue === targetValue) return; // Ajouter l'animation pop element.classList.add("updating"); setTimeout(() => { element.classList.remove("updating"); }, 400); // Durée de l'animation synchronisée avec la barre (800ms) const duration = 800; // Même durée que la transition CSS de la barre const steps = 30; // Plus de steps pour une animation plus fluide const increment = (targetValue - currentValue) / steps; const stepDuration = duration / steps; let current = currentValue; let step = 0; const interval = setInterval(() => { step++; current += increment; if (step >= steps) { element.textContent = `-${targetValue}%`; clearInterval(interval); } else { element.textContent = `-${Math.round(current)}%`; } }, stepDuration); } /** Rendu des items Lizia (couleur + personnalisation) — pack combo uniquement */ function renderLiziaItems() { if (!liziaItemsContainer) return; const isCombo = state.pack === PACK_COMBO; if (colorsPersoLabel) { colorsPersoLabel.innerHTML = isCombo ? '2 Farbe & Gravur' : '2 Farbe'; } if (!isCombo) { liziaItemsContainer.innerHTML = ""; liziaItemsContainer.style.display = "none"; return; } liziaItemsContainer.style.display = ""; renderLiziaItemsContent(); } /** Contenu du rendu des items Lizia (séparé pour la réutilisation) */ function renderLiziaItemsContent() { const quantityIncreased = state.quantity > previousQuantity; const newItemsStartIndex = quantityIncreased ? previousQuantity : 0; liziaItemsContainer.innerHTML = Array.from({ length: state.quantity }) .map((_, index) => { // Ajouter l'animation seulement aux nouveaux items const shouldAnimate = quantityIncreased && index >= newItemsStartIndex; const animationClass = shouldAnimate ? " fadeInSlide" : ""; const animationDelay = shouldAnimate ? `style="animation-delay: ${(index - newItemsStartIndex) * 0.1}s"` : ""; return `
    Lizia
    ${LIZIA_COLORS .map( (color) => ` `, ) .join("")}
    ${ index === 0 ? '
    Gravur (optional)
    +4.95€
    ' : "" }
    20 ? "warning" : "" }"> ${ (state.personalization[index] || "") .length }/25
    Lizia
    ${LIZIA_COLORS .map( (color) => ` `, ) .join("")}
    Gravur (optional)
    +4.95€
    20 ? "warning" : "" }"> ${ (state.personalization[index] || "").length }/25
    `; }) .join(""); } /** Rendu du prix sur les boutons panier */ function renderPrice() { const pack = PACKS.find((p) => p.id === state.pack) || PACKS[0]; let unitPrice = pack.price; let originalUnitPrice = pack.originalPrice || pack.price; const isPersonalized = state.pack === PACK_COMBO && state.personalization[0] && state.personalization[0].trim().length > 0; if (isPersonalized) { unitPrice += PERSONALIZATION_PRICE; originalUnitPrice += PERSONALIZATION_PRICE; } const totalPrice = unitPrice; const totalReferencePrice = originalUnitPrice; const savings = totalReferencePrice - totalPrice; let buttonText = `IN DEN WARENKORB • ${totalPrice.toFixed(2)}€`; if (savings > 0.01) { buttonText = `IN DEN WARENKORB • ${totalReferencePrice.toFixed( 2, )}€${totalPrice.toFixed(2)}€`; } addToCartBtn.innerHTML = buttonText; if (addToCartBtnMobile) { addToCartBtnMobile.innerHTML = buttonText; } } /** Afficher la popup de détails (non utilisée pour housses) */ function showPackPopup(packId) { // Non utilisé pour les housses return; } /** Masquer la popup */ function hidePackPopup() { packPopup.classList.remove("show"); document.body.style.overflow = "auto"; } // --- FONCTION DE MISE À JOUR GLOBALE --- function updateUI() { renderImageGallery(); renderPackSelector(); renderColorSelector(); renderLiziaItems(); renderPrice(); addEventListeners(); } // --- FONCTION DE MISE À JOUR OPTIMISÉE --- function updateUIOptimized(forceImageReload = false) { // Ne recharger les images que si nécessaire if (forceImageReload) { renderImageGallery(); } else { // Si pas de rechargement d'images, juste mettre à jour les vignettes updateThumbnailsOnly(); } renderPackSelector(); renderColorSelector(); renderLiziaItems(); renderPrice(); addEventListeners(); } // --- FONCTION DE NAVIGATION D'IMAGES AVEC SLIDE --- function updateImageNavigation(direction = null) { const currentImages = getProductImages(); const currentImageUrl = currentImages[state.currentImageIndex]; const isVideo = currentImageUrl && currentImageUrl.startsWith("VIDEO:"); const actualUrl = isVideo ? currentImageUrl.replace("VIDEO:", "") : currentImageUrl; // Détecter ce qui est actuellement affiché const videoElement = mainImageContainer ? mainImageContainer.querySelector("video.mainImage") : null; const videoDisplay = videoElement ? window.getComputedStyle(videoElement).display : "none"; const imageDisplay = mainImage ? window.getComputedStyle(mainImage).display : "none"; const currentIsVideo = videoElement && videoDisplay !== "none"; const currentIsImage = mainImage && imageDisplay !== "none"; // Si pas de direction (clic sur miniature), changement direct if (!direction) { if (isVideo) { // Masquer l'image si elle existe if (mainImage) { mainImage.style.display = "none"; } // Vérifier si une vidéo existe déjà let finalVideoElement = mainImageContainer ? mainImageContainer.querySelector("video.mainImage") : null; if (!finalVideoElement) { // Créer une nouvelle vidéo finalVideoElement = document.createElement("video"); finalVideoElement.className = "mainImage"; finalVideoElement.setAttribute("playsinline", ""); finalVideoElement.setAttribute("muted", ""); finalVideoElement.setAttribute("autoplay", ""); finalVideoElement.setAttribute("loop", ""); finalVideoElement.setAttribute("controls", ""); finalVideoElement.style.width = "100%"; finalVideoElement.style.height = "100%"; finalVideoElement.style.objectFit = "cover"; finalVideoElement.style.position = "absolute"; finalVideoElement.style.top = "0"; finalVideoElement.style.left = "0"; finalVideoElement.style.display = "block"; finalVideoElement.style.cursor = "pointer"; finalVideoElement.style.backgroundColor = "#000"; // Fond noir pour éviter le fond rose const source = document.createElement("source"); source.src = actualUrl; source.type = "video/mp4"; finalVideoElement.appendChild(source); if (mainImageContainer) { mainImageContainer.appendChild(finalVideoElement); } } else { // Utiliser la vidéo existante finalVideoElement.style.display = "block"; finalVideoElement.style.left = "0"; finalVideoElement.style.transition = ""; // Vérifier si la source doit être mise à jour const source = finalVideoElement.querySelector("source"); const currentSrc = source ? source.src : ""; if (!source || currentSrc !== actualUrl) { // Mettre à jour la source finalVideoElement.innerHTML = ""; const newSource = document.createElement("source"); newSource.src = actualUrl; newSource.type = "video/mp4"; finalVideoElement.appendChild(newSource); finalVideoElement.currentTime = 0; finalVideoElement.load(); finalVideoElement.play().catch((err) => { console.warn("Erreur de lecture vidéo:", err); }); } else { // Réinitialiser à 0 finalVideoElement.currentTime = 0; if (finalVideoElement.paused) { finalVideoElement.play().catch((err) => { console.warn("Erreur de lecture vidéo:", err); }); } } } // Ajouter les contrôles vidéo (play/pause + barre de progression) setupVideoControls(finalVideoElement); } else { // Masquer la vidéo si elle existe if (videoElement) { videoElement.style.display = "none"; videoElement.pause(); videoElement.currentTime = 0; } // Afficher l'image if (mainImage) { mainImage.style.display = "block"; mainImage.src = actualUrl; mainImage.style.left = "0"; mainImage.style.transition = ""; } } // Mettre à jour les dots et miniatures updateDotsAndThumbnails(); // Réattacher les event listeners addEventListeners(); return; } // Si direction est fournie (flèche ou swipe), animer la transition const container = mainImageContainer || (mainImage ? mainImage.parentElement : null); if (!container) { renderImageGallery(); updateDotsAndThumbnails(); addEventListeners(); return; } // Préparer l'élément actuel pour l'animation let currentElement; if (currentIsVideo && videoElement) { currentElement = videoElement; } else if (currentIsImage && mainImage) { currentElement = mainImage; } // Fonction pour animer le slide function animateSlide(tempEl) { // Forcer le reflow tempEl.offsetHeight; // Animer les deux éléments ensemble tempEl.style.transition = "left 0.3s ease-out"; if (currentElement) { currentElement.style.transition = "left 0.3s ease-out"; } if (direction === "left") { // Swipe vers la gauche : nouveau élément arrive de la droite if (currentElement) { currentElement.style.left = "-100%"; } tempEl.style.left = "0"; } else { // Swipe vers la droite : nouveau élément arrive de la gauche if (currentElement) { currentElement.style.left = "100%"; } tempEl.style.left = "0"; } } // Créer l'élément temporaire pour la transition let tempElement; if (isVideo) { // Créer une vidéo temporaire tempElement = document.createElement("video"); tempElement.className = "mainImage"; tempElement.setAttribute("playsinline", ""); tempElement.setAttribute("muted", ""); tempElement.setAttribute("autoplay", ""); tempElement.setAttribute("loop", ""); tempElement.setAttribute("controls", ""); tempElement.style.width = "100%"; tempElement.style.height = "100%"; tempElement.style.objectFit = "cover"; tempElement.style.position = "absolute"; tempElement.style.top = "0"; tempElement.style.left = direction === "left" ? "100%" : "-100%"; tempElement.style.cursor = "pointer"; tempElement.style.backgroundColor = "#000"; // Fond noir pour éviter le fond rose const source = document.createElement("source"); source.src = actualUrl; source.type = "video/mp4"; tempElement.appendChild(source); // Ajouter pause/play au clic tempElement.addEventListener("click", () => { if (tempElement.paused) { tempElement.play().catch(() => {}); } else { tempElement.pause(); } }); container.appendChild(tempElement); // Charger la vidéo avant d'animer tempElement.currentTime = 0; tempElement.load(); // Attendre que la vidéo soit prête avant d'animer const startAnimation = () => { tempElement.play().catch(() => {}); animateSlide(tempElement); }; if (tempElement.readyState >= 2) { // La vidéo est déjà chargée startAnimation(); } else { // Attendre que la vidéo soit chargée tempElement.addEventListener("loadeddata", startAnimation, { once: true, }); tempElement.addEventListener("canplay", startAnimation, { once: true }); // Timeout de sécurité setTimeout(startAnimation, 100); } } else { // Créer une image temporaire tempElement = document.createElement("img"); tempElement.className = "mainImage"; tempElement.src = actualUrl; tempElement.style.position = "absolute"; tempElement.style.top = "0"; tempElement.style.width = "100%"; tempElement.style.height = "100%"; tempElement.style.objectFit = "cover"; tempElement.style.left = direction === "left" ? "100%" : "-100%"; container.appendChild(tempElement); // Pour les images, animer directement animateSlide(tempElement); } // Après l'animation, nettoyer et afficher le bon élément setTimeout(() => { if (isVideo) { // Masquer l'image si elle existe if (mainImage) { mainImage.style.display = "none"; } // Supprimer toutes les vidéos existantes sauf celle temporaire const existingVideos = mainImageContainer.querySelectorAll("video.mainImage"); existingVideos.forEach((vid) => { if (vid !== tempElement && container.contains(vid)) { vid.pause(); container.removeChild(vid); } }); // Transformer l'élément temporaire en élément permanent tempElement.style.transition = ""; tempElement.style.left = "0"; // Ajouter les contrôles vidéo (play/pause + barre de progression) setupVideoControls(tempElement); } else { // Masquer la vidéo si elle existe if (videoElement) { videoElement.style.display = "none"; videoElement.pause(); videoElement.currentTime = 0; } // Afficher l'image if (mainImage) { mainImage.style.display = "block"; mainImage.src = actualUrl; mainImage.style.transition = ""; mainImage.style.left = "0"; } // Supprimer l'élément temporaire if (container.contains(tempElement)) { container.removeChild(tempElement); } } // Réinitialiser les styles de l'élément actuel if (currentElement && currentElement !== tempElement) { currentElement.style.transition = ""; currentElement.style.left = ""; } // Mettre à jour les dots et miniatures updateDotsAndThumbnails(); // Réattacher les event listeners addEventListeners(); }, 300); } // Fonction helper pour mettre à jour les dots et miniatures function updateDotsAndThumbnails() { const dots = document.querySelectorAll(".dot"); dots.forEach((dot, index) => { if (index === state.currentImageIndex) { dot.classList.add("active"); } else { dot.classList.remove("active"); } }); const thumbnails = document.querySelectorAll(".thumbnailBtn"); thumbnails.forEach((thumb, index) => { if (index === state.currentImageIndex) { thumb.classList.add("active"); } else { thumb.classList.remove("active"); } }); } // --- FONCTION DE MISE À JOUR DES VIGNETTES SANS FLASH --- function updateThumbnailsOnly() { // Mettre à jour seulement les classes des vignettes existantes const thumbnails = document.querySelectorAll(".thumbnailBtn"); thumbnails.forEach((thumb, index) => { if (index === state.currentImageIndex) { thumb.classList.add("active"); } else { thumb.classList.remove("active"); } }); } // --- FONCTION POUR METTRE À JOUR LES IMAGES ET VIGNETTES --- function updateImagesAndThumbnails() { // Utiliser renderImageGallery pour gérer les vidéos correctement renderImageGallery(); return; // Mettre à jour les vignettes avec les nouvelles images const thumbnails = document.querySelectorAll(".thumbnailBtn"); thumbnails.forEach((thumb, thumbIndex) => { const img = thumb.querySelector("img"); if (img && currentImages[thumbIndex]) { img.src = currentImages[thumbIndex]; } if (thumbIndex === state.currentImageIndex) { thumb.classList.add("active"); } else { thumb.classList.remove("active"); } }); // Mettre à jour les dots const dots = document.querySelectorAll(".dot"); dots.forEach((dot, dotIndex) => { if (dotIndex === state.currentImageIndex) { dot.classList.add("active"); } else { dot.classList.remove("active"); } }); } // --- FONCTIONS D'AJOUT AU PANIER --- /** * Valide et filtre le texte de personnalisation * Autorise : lettres avec accents, chiffres, espaces, emojis cœur, guillemets */ function validatePersonalizationText(text) { const regex = /^[a-zA-ZÀ-ÿ0-9\s❤️"']+$/; if (!regex.test(text)) { // Supprimer les caractères non autorisés return text.replace(/[^a-zA-ZÀ-ÿ0-9\s❤️"']+/g, ""); } return text; } /** * Récupère l'ID du variant Shopify selon le pack, les couleurs et la personnalisation */ function getVariantID(productID, color, isPersonalized) { if (productID === PRODUCT_ID) { const colorKey = color || state.color || "bleu"; return variantIDs[productID]?.[colorKey]?.normal || null; } if (productID === COMBO_PRODUCT_A || productID === COMBO_PRODUCT_B) { const housseKey = capitalizeColor(state.color === "rose" ? "rose" : "bleu"); const liziaKey = capitalizeColor(color || state.colors[0] || "blanc"); const comboMap = variantIDs[productID]?.[housseKey]?.[liziaKey]; if (!comboMap) { console.error( `Variant combo non trouvé pour housse ${housseKey}, lizia ${liziaKey}`, ); return null; } return isPersonalized ? comboMap.personnalise : comboMap.normal; } console.error( `Variant non trouvé pour produit ${productID} et couleur ${color}`, ); return null; } function capitalizeColor(color) { if (!color) return ""; return color.charAt(0).toUpperCase() + color.slice(1); } // --- LOGIQUE D'AJOUT AU PANIER (copie de Lizia Vacances / AB pricing) --- async function handleAddToCart() { const packID = packIDMapping[state.pack]; if (!packID) { console.error("Pack ID introuvable pour", state.pack); return; } let firstVariantID; let firstIsPersonalized = false; let personalizeInputID; if (state.pack === PACK_COMBO) { const firstColor = state.colors[0]; firstIsPersonalized = state.personalization[0] && state.personalization[0].trim().length > 0; firstVariantID = getVariantID(packID, firstColor, firstIsPersonalized); personalizeInputID = packID === COMBO_PRODUCT_B ? COMBO_FORM_INPUT_B : COMBO_FORM_INPUT_A; } else { firstVariantID = getVariantID(packID, state.color || "bleu", false); } if (!firstVariantID) { console.error( "Impossible de récupérer le variant pour le premier produit", ); return; } // Sélectionner le variant dans TOUS les selects (thème + formulaire caché) const optionSelector = `#ProductSelect_${packID}TEST option[value="${firstVariantID}"], #ProductSelect_${packID} option[value="${firstVariantID}"]`; const optionMatches = document.querySelectorAll(optionSelector); if (optionMatches.length > 0) { optionMatches.forEach((opt) => { opt.selected = true; }); } else { console.error( "Option de sélection non trouvée pour le variant:", firstVariantID, ); } // Définir le texte de personnalisation const personalizeInput = document.getElementById(personalizeInputID); if (personalizeInput) { personalizeInput.value = firstIsPersonalized ? state.personalization[0] : ""; } // Comme Lizia : cliquer le bouton ATC du formulaire qui gère ce pack. // - Pack = produit de la page → le bouton est dans le premier #prodForm (c'est lui qui ouvre le slide) // - Pack secondaire (housse) → le bouton du formulaire caché (comme Vacances) const firstProdForm = document.getElementById("prodForm"); const themePackButton = firstProdForm ? firstProdForm.querySelector(`.add_to_cart_btn_${packID}`) : null; let addToCartButton = themePackButton; if (!addToCartButton) { const hiddenSelect = document.querySelector(`#ProductSelect_${packID}TEST`) || document.querySelector(`#ProductSelect_${packID}`); addToCartButton = hiddenSelect ? hiddenSelect.closest("form")?.querySelector(`.add_to_cart_btn_${packID}`) : document.getElementsByClassName(`add_to_cart_btn_${packID}`)[0]; } if (!addToCartButton) { console.error( "Bouton d'ajout au panier introuvable pour le pack:", packID, ); return; } addToCartButton.removeAttribute("disabled"); addToCartButton.classList.remove("disabled"); if (window.jQuery) { window.jQuery(addToCartButton).trigger("click"); } else { addToCartButton.click(); } } /** * Ajoute plusieurs produits au panier via l'API Shopify */ function addMultipleToCart(products) { const items = products.map((product) => ({ id: product.variantID, quantity: product.quantity, properties: product.properties, })); return fetch("/cart/add.js", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify({ items }), }) .then((response) => { if (!response.ok) { return Promise.reject("Erreur de réponse du serveur"); } return response.json(); }) .then((data) => { console.log("Produits ajoutés au panier:", data); return data; }) .catch((error) => { console.error("Erreur lors de l'ajout au panier:", error); throw error; }); } // --- GESTIONNAIRES D'ÉVÉNEMENTS --- function addEventListeners() { // Galerie d'images prevBtn.onclick = () => { const currentImages = getProductImages(); state.currentImageIndex = (state.currentImageIndex - 1 + currentImages.length) % currentImages.length; updateImageNavigation("right"); // Image vient de la gauche }; nextBtn.onclick = () => { const currentImages = getProductImages(); state.currentImageIndex = (state.currentImageIndex + 1) % currentImages.length; updateImageNavigation("left"); // Image vient de la droite }; document.querySelectorAll(".dot, .thumbnailBtn").forEach((el) => { el.onclick = (e) => { state.currentImageIndex = parseInt(e.currentTarget.dataset.index); updateImageNavigation(); // Fonction optimisée pour la navigation }; }); // Gestion du swipe sur mobile pour la galerie d'images const mainImageContainer = document.querySelector(".mainImageContainer"); if (mainImageContainer) { let touchStartX = 0; let touchStartY = 0; let currentTouchX = 0; let isDragging = false; let isHorizontalSwipe = false; let nextImage = null; let prevImage = null; let isAnimating = false; const minSwipeDistance = 50; // Distance minimale pour valider le swipe mainImageContainer.addEventListener( "touchstart", (e) => { if (isAnimating) return; // Empêcher le swipe pendant une animation touchStartX = e.touches[0].clientX; touchStartY = e.touches[0].clientY; currentTouchX = touchStartX; isDragging = true; isHorizontalSwipe = false; // Nettoyer les images précédentes au cas où cleanupSwipeImages(); // Créer les images voisines pour le swipe const currentImages = getProductImages(); const container = mainImage.parentElement; // Image suivante (à droite) nextImage = document.createElement("img"); const nextIndex = (state.currentImageIndex + 1) % currentImages.length; nextImage.src = currentImages[nextIndex]; nextImage.className = "mainImage swipe-temp-image"; nextImage.style.position = "absolute"; nextImage.style.top = "0"; nextImage.style.left = "100%"; nextImage.style.width = "100%"; nextImage.style.height = "100%"; nextImage.style.objectFit = "cover"; nextImage.style.transition = "none"; nextImage.style.pointerEvents = "none"; container.appendChild(nextImage); // Image précédente (à gauche) prevImage = document.createElement("img"); const prevIndex = (state.currentImageIndex - 1 + currentImages.length) % currentImages.length; prevImage.src = currentImages[prevIndex]; prevImage.className = "mainImage swipe-temp-image"; prevImage.style.position = "absolute"; prevImage.style.top = "0"; prevImage.style.left = "-100%"; prevImage.style.width = "100%"; prevImage.style.height = "100%"; prevImage.style.objectFit = "cover"; prevImage.style.transition = "none"; prevImage.style.pointerEvents = "none"; container.appendChild(prevImage); // Désactiver la transition pendant le drag mainImage.style.transition = "none"; }, { passive: true }, ); mainImageContainer.addEventListener( "touchmove", (e) => { if (!isDragging || isAnimating) return; currentTouchX = e.touches[0].clientX; const currentTouchY = e.touches[0].clientY; const deltaX = currentTouchX - touchStartX; const deltaY = currentTouchY - touchStartY; // Détecter si c'est un swipe horizontal ou vertical if (!isHorizontalSwipe && Math.abs(deltaX) > 10) { if (Math.abs(deltaX) > Math.abs(deltaY)) { isHorizontalSwipe = true; } } // Si c'est un swipe horizontal, bloquer le scroll vertical if (isHorizontalSwipe) { e.preventDefault(); const containerWidth = mainImageContainer.offsetWidth; const percentage = (deltaX / containerWidth) * 100; // Déplacer les trois images en fonction du swipe mainImage.style.left = `${percentage}%`; if (nextImage) nextImage.style.left = `${100 + percentage}%`; if (prevImage) prevImage.style.left = `${-100 + percentage}%`; } }, { passive: false }, ); // passive: false pour pouvoir preventDefault mainImageContainer.addEventListener( "touchend", (e) => { if (!isDragging || isAnimating) return; // Si ce n'était pas un swipe horizontal, ne rien faire if (!isHorizontalSwipe) { cleanupSwipeImages(); isDragging = false; return; } isAnimating = true; const swipeDistance = currentTouchX - touchStartX; const containerWidth = mainImageContainer.offsetWidth; const swipePercentage = Math.abs(swipeDistance / containerWidth); // Réactiver les transitions mainImage.style.transition = "left 0.3s ease-out"; if (nextImage) nextImage.style.transition = "left 0.3s ease-out"; if (prevImage) prevImage.style.transition = "left 0.3s ease-out"; const currentImages = getProductImages(); // Si le swipe est assez grand, changer d'image if ( swipePercentage > 0.25 || Math.abs(swipeDistance) > minSwipeDistance ) { if (swipeDistance > 0) { // Swipe vers la droite = image précédente const newIndex = (state.currentImageIndex - 1 + currentImages.length) % currentImages.length; mainImage.style.left = "100%"; if (prevImage) prevImage.style.left = "0"; if (nextImage) nextImage.style.left = "200%"; setTimeout(() => { state.currentImageIndex = newIndex; mainImage.src = currentImages[state.currentImageIndex]; mainImage.style.transition = ""; mainImage.style.left = "0"; cleanupSwipeImages(); updateImageDots(); isAnimating = false; }, 300); } else { // Swipe vers la gauche = image suivante const newIndex = (state.currentImageIndex + 1) % currentImages.length; mainImage.style.left = "-100%"; if (nextImage) nextImage.style.left = "0"; if (prevImage) prevImage.style.left = "-200%"; setTimeout(() => { state.currentImageIndex = newIndex; mainImage.src = currentImages[state.currentImageIndex]; mainImage.style.transition = ""; mainImage.style.left = "0"; cleanupSwipeImages(); updateImageDots(); isAnimating = false; }, 300); } } else { // Swipe trop petit, revenir à la position initiale mainImage.style.left = "0"; if (nextImage) nextImage.style.left = "100%"; if (prevImage) prevImage.style.left = "-100%"; setTimeout(() => { cleanupSwipeImages(); isAnimating = false; }, 300); } isDragging = false; }, { passive: true }, ); function cleanupSwipeImages() { const container = mainImage.parentElement; // Nettoyer toutes les images temporaires const tempImages = container.querySelectorAll(".swipe-temp-image"); tempImages.forEach((img) => { if (img.parentElement) { container.removeChild(img); } }); nextImage = null; prevImage = null; } function updateImageDots() { // Mettre à jour les dots const dots = document.querySelectorAll(".dot"); dots.forEach((dot, index) => { if (index === state.currentImageIndex) { dot.classList.add("active"); } else { dot.classList.remove("active"); } }); // Mettre à jour les miniatures const thumbnails = document.querySelectorAll(".thumbnailBtn"); thumbnails.forEach((thumb, index) => { if (index === state.currentImageIndex) { thumb.classList.add("active"); } else { thumb.classList.remove("active"); } }); } } // Sélecteur de pack document.querySelectorAll(".packOption, .packBadgeTop").forEach((el) => { el.onclick = () => { const packId = el.dataset.pack; if (!packId || state.pack === packId) return; state.pack = packId; state.currentImageIndex = 0; updateUIOptimized(true); }; }); // Sélecteur de couleur housse (vignettes) if (colorSelectorContainer && !colorSelectorContainer._colorDelegate) { colorSelectorContainer._colorDelegate = true; colorSelectorContainer.addEventListener("click", (e) => { const btn = e.target.closest("[data-housse-color]"); if (!btn) return; const colorId = btn.dataset.housseColor; if (colorId && state.color !== colorId) { state.color = colorId; state.currentImageIndex = 0; updateUIOptimized(true); } }); } // Couleur Lizia document.querySelectorAll(".colorBtn, .colorBtnMobile").forEach((el) => { el.onclick = () => { const colorId = el.dataset.color; const index = parseInt(el.dataset.index, 10) || 0; if (!colorId) return; state.colors[index] = colorId; if (state.pack === PACK_COMBO) { state.currentImageIndex = 0; updateUIOptimized(true); } else { updateUIOptimized(false); } }; }); // Personnalisation / gravure document.querySelectorAll(".persoInput, .persoInputMobile").forEach((el) => { el.oninput = (e) => { const index = parseInt(e.currentTarget.dataset.index, 10) || 0; state.personalization[index] = validatePersonalizationText(e.currentTarget.value); const filtered = state.personalization[index]; if (filtered !== e.currentTarget.value) { e.currentTarget.value = filtered; } state.personalization[index] = filtered; const wrapper = e.currentTarget.closest( ".inputContainer, .inputContainerMobile", ); const counter = wrapper?.querySelector( ".charCounter, .charCounterMobile", ); if (counter) { counter.textContent = `${filtered.length}/25`; counter.classList.toggle("warning", filtered.length > 20); } e.currentTarget.classList.toggle("has-content", filtered.trim().length > 0); renderPrice(); }; }); // Fermeture de la popup (si elle existe) if (popupClose) { popupClose.onclick = hidePackPopup; } if (packPopup) { packPopup.onclick = (e) => { if (e.target === packPopup) { hidePackPopup(); } }; } // Force focus on mobile input document .querySelectorAll( ".persoInputWrapperMobile, .inputContainerMobile, .persoInputMobile", ) .forEach((el) => { el.addEventListener("click", (e) => { const wrapper = e.currentTarget.closest(".persoInputWrapperMobile"); if (wrapper) { const input = wrapper.querySelector(".persoInputMobile"); if (input) { input.focus(); } } }); }); } // --- INITIALISATION --- updateUI(); // Précharger les images des couleurs pour éviter le lag preloadColorImages(); // Initialiser la section avis renderReviewsSection(); // Initialiser le lazy loading optimisé setTimeout(() => { loadVisibleImages(); }, 100); // Observer pour le lazy loading avec Intersection Observer const imageObserver = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { const element = entry.target; // Gérer les images if ( element.tagName === "IMG" && element.dataset.src && !element.src ) { // Charger directement l'image sans délai element.src = element.dataset.src; element.classList.add("loaded"); imageObserver.unobserve(element); } // Gérer les vidéos dans les miniatures - seulement charger la première frame (preload="metadata") else if ( element.tagName === "VIDEO" && element.classList.contains("videoPreview") ) { // Ne rien faire - la vidéo dans les miniatures utilise preload="metadata" // pour charger seulement la première frame, mais ne se lance pas automatiquement imageObserver.unobserve(element); } } }); }, { rootMargin: "100px 0px", // Charger 100px avant que l'image soit visible threshold: 0.1, }, ); // Observer toutes les images lazy setTimeout(() => { const lazyImages = document.querySelectorAll(".lazy-image"); lazyImages.forEach((img) => imageObserver.observe(img)); }, 200); // Attacher les événements aux deux boutons addToCartBtn.addEventListener("click", handleAddToCart); if (addToCartBtnMobile) { addToCartBtnMobile.addEventListener("click", handleAddToCart); } // --- GESTION DES AVIS --- function formatReviewDate(month, year) { // Capitalize first letter and ensure max 4 characters const formattedMonth = month.charAt(0).toUpperCase() + month.slice(1); return `${formattedMonth.substring(0, 4)}. ${year}`; } function renderReviewsSection() { const container = document.getElementById("reviewsContainer"); if (!container) return; // Dupliquer les cartes pour l'effet infini (3 fois : avant, milieu, après) const tripleData = [...reviewsData, ...reviewsData, ...reviewsData]; // Generate review cards HTML const reviewsHTML = tripleData .map((review) => { const stars = "★".repeat(review.rating); const firstLetter = review.firstName.charAt(0).toUpperCase(); const formattedDate = formatReviewDate( review.date.month, review.date.year, ); return `
    "
    ${firstLetter}
    ${review.firstName} ${ review.lastName }.
    ${review.gender}
    ${stars}
    ${formattedDate}

    ${review.title}

    ${review.content}

    ${ review.verified ? '
    Vérifié
    ' : "" }
    `; }) .join(""); container.innerHTML = reviewsHTML; // Navigation arrows are now handled by HTML/CSS and initReviewsNavigation function // Render navigation dots renderReviewsDots(); // Initialize swipe navigation initReviewsSwipe(); } function renderReviewsDots() { const dotsContainer = document.getElementById("reviewsDots"); if (!dotsContainer) return; // Calculer le nombre de dots : on exclut le premier et le dernier // Sur desktop, on voit 3 cartes à la fois, donc reviewsData.length - 2 dots const numDots = Math.max(1, reviewsData.length - 2); const dotsHTML = Array.from({ length: numDots }) .map( (_, index) => ` `, ) .join(""); dotsContainer.innerHTML = dotsHTML; // Add click listeners to dots const dots = dotsContainer.querySelectorAll(".reviewDot"); dots.forEach((dot) => { dot.addEventListener("click", () => { const index = parseInt(dot.dataset.index); scrollToReviewByDot(index); }); }); } function scrollToReviewByDot(dotIndex) { const container = document.getElementById("reviewsContainer"); if (!container) return; const cards = container.querySelectorAll(".reviewCard"); // Commencer au milieu du set dupliqué + 1 pour sauter la première carte const targetIndex = reviewsData.length + dotIndex + 1; if (cards[targetIndex]) { container.scrollTo({ left: cards[targetIndex].offsetLeft - container.offsetLeft, behavior: "smooth", }); } } function scrollToReview(index) { const container = document.getElementById("reviewsContainer"); if (!container) return; const cards = container.querySelectorAll(".reviewCard"); if (cards[index]) { cards[index].scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center", }); } } function updateReviewsDots() { const container = document.getElementById("reviewsContainer"); const dotsContainer = document.getElementById("reviewsDots"); if (!container || !dotsContainer) return; const cards = Array.from(container.querySelectorAll(".reviewCard")); const dots = Array.from(dotsContainer.querySelectorAll(".reviewDot")); // Calculate which card is currently in view const containerRect = container.getBoundingClientRect(); const containerCenter = containerRect.left + containerRect.width / 2; let activeIndex = 0; let minDistance = Infinity; cards.forEach((card, index) => { const cardRect = card.getBoundingClientRect(); const cardCenter = cardRect.left + cardRect.width / 2; const distance = Math.abs(cardCenter - containerCenter); if (distance < minDistance) { minDistance = distance; activeIndex = index; } }); // Mapper l'index de la carte à l'index du dot // On a 3x reviewsData.length cartes, donc on module par reviewsData.length let dotIndex = (activeIndex % reviewsData.length) - 1; // -1 car on exclut la première // Ajuster si nécessaire if (dotIndex < 0) dotIndex = 0; if (dotIndex >= dots.length) dotIndex = dots.length - 1; // Update dots - simple : actif ou inactif dots.forEach((dot, index) => { if (index === dotIndex) { dot.classList.add("active"); } else { dot.classList.remove("active"); } }); } function initReviewsSwipe() { const container = document.getElementById("reviewsContainer"); if (!container) return; const cards = container.querySelectorAll(".reviewCard"); if (cards.length === 0) return; // Calculer la largeur d'une section (set original) const sectionWidth = container.scrollWidth / 3; // Positionner au centre du set dupliqué (milieu) container.scrollLeft = sectionWidth; // Gestion de l'infinite scroll function handleInfiniteScroll() { const scrollLeft = container.scrollLeft; const maxScroll = container.scrollWidth - container.clientWidth; // Si on arrive à la fin, revenir au milieu if (scrollLeft >= maxScroll - 10) { container.scrollLeft = sectionWidth + (scrollLeft - maxScroll + sectionWidth); } // Si on arrive au début, aller à la fin du milieu else if (scrollLeft <= 10) { container.scrollLeft = sectionWidth + scrollLeft; } } // Update dots on scroll avec infinite scroll - EN TEMPS RÉEL let scrollTimeout; let isUserScrolling = false; container.addEventListener("scroll", () => { isUserScrolling = true; // Update dots immédiatement pendant le scroll updateReviewsDots(); clearTimeout(scrollTimeout); scrollTimeout = setTimeout(() => { handleInfiniteScroll(); isUserScrolling = false; }, 150); }); // Touch swipe support (mobile) - simple comme le drag PC let isTouchDragging = false; container.addEventListener("touchstart", () => { isTouchDragging = true; }); container.addEventListener("touchend", () => { isTouchDragging = false; }); // Mouse drag support (desktop) - simple, comme le swipe mobile let isMouseDown = false; let startX; let scrollLeft; container.addEventListener("mousedown", (e) => { isMouseDown = true; startX = e.pageX - container.offsetLeft; scrollLeft = container.scrollLeft; container.style.cursor = "grabbing"; }); container.addEventListener("mouseleave", () => { isMouseDown = false; container.style.cursor = "grab"; }); container.addEventListener("mouseup", () => { isMouseDown = false; container.style.cursor = "grab"; }); container.addEventListener("mousemove", (e) => { if (!isMouseDown) return; e.preventDefault(); const x = e.pageX - container.offsetLeft; const walk = (x - startX) * 1.5; container.scrollLeft = scrollLeft - walk; }); // Initialize dots updateReviewsDots(); } // --- GESTION DES VIDÉOS --- // Fonction pour mettre à jour l'icône play/pause (accessible globalement) function updatePlayButton(videoId, isPlaying) { const button = document.querySelector( `.videoPlayBtn[data-video-id="${videoId}"]`, ); if (!button) return; const playIcon = button.querySelector(".playIcon"); const pauseIcon = button.querySelector(".pauseIcon"); if (isPlaying) { button.style.opacity = "0"; button.style.pointerEvents = "none"; } else { button.style.opacity = "1"; button.style.pointerEvents = "auto"; playIcon.style.display = "block"; if (pauseIcon) pauseIcon.style.display = "none"; } } function initVideoControls() { const videos = document.querySelectorAll(".liziaVideo"); const playButtons = document.querySelectorAll(".videoPlayBtn"); const muteButtons = document.querySelectorAll(".videoMuteBtn"); const videoContainers = document.querySelectorAll(".videoContainer"); // Fonction pour mettre en pause toutes les autres vidéos function pauseOtherVideos(currentVideoId) { videos.forEach((video) => { const videoId = video.dataset.videoId; if (videoId !== currentVideoId && !video.paused) { video.pause(); updatePlayButton(videoId, false); const playbackState = videoPlaybackStates[videoId]; if (playbackState) { playbackState.userPaused = false; } } }); } // Fonction pour mettre à jour l'icône mute/unmute function updateMuteButton(videoId, isMuted) { const button = document.querySelector( `.videoMuteBtn[data-video-id="${videoId}"]`, ); if (!button) return; const unmuteIcon = button.querySelector(".unmuteIcon"); const muteIcon = button.querySelector(".muteIcon"); if (isMuted) { unmuteIcon.style.display = "none"; muteIcon.style.display = "block"; } else { unmuteIcon.style.display = "block"; muteIcon.style.display = "none"; } } // Fonction pour toggle play/pause function togglePlay(video) { const videoId = video.dataset.videoId; const playbackState = videoPlaybackStates[videoId] || (videoPlaybackStates[videoId] = { hasAutoPlayed: false, userPaused: false, }); if (video.paused) { // Mettre en pause toutes les autres vidéos pauseOtherVideos(videoId); video .play() .then(() => { playbackState.hasAutoPlayed = true; playbackState.userPaused = false; updatePlayButton(videoId, true); }) .catch((err) => console.warn("Lecture vidéo impossible:", err)); } else { video.pause(); playbackState.userPaused = true; updatePlayButton(videoId, false); } } // Fonction pour toggle mute/unmute function toggleMute(video) { const videoId = video.dataset.videoId; video.muted = !video.muted; updateMuteButton(videoId, video.muted); } // Initialiser toutes les vidéos videos.forEach((video) => { const videoId = video.dataset.videoId; const playbackState = videoPlaybackStates[videoId] || (videoPlaybackStates[videoId] = { hasAutoPlayed: false, userPaused: false, }); // Initialiser l'état du bouton mute selon l'attribut HTML ou la propriété updateMuteButton(videoId, video.muted); // Event listener pour la fin de la vidéo video.addEventListener("ended", () => { updatePlayButton(videoId, false); }); // Mettre à jour l'icône et unmute automatique quand la vidéo commence à jouer video.addEventListener("play", () => { updatePlayButton(videoId, true); playbackState.hasAutoPlayed = true; playbackState.userPaused = false; if (video.muted) { video.muted = false; updateMuteButton(videoId, false); } }); // Mettre à jour l'icône quand la vidéo est en pause video.addEventListener("pause", () => { updatePlayButton(videoId, false); }); }); // Event listeners pour les boutons play/pause playButtons.forEach((button) => { button.addEventListener("click", (e) => { e.stopPropagation(); const videoId = button.dataset.videoId; const video = document.querySelector( `.liziaVideo[data-video-id="${videoId}"]`, ); if (video) { togglePlay(video); } }); }); // Event listeners pour les boutons mute/unmute muteButtons.forEach((button) => { button.addEventListener("click", (e) => { e.stopPropagation(); const videoId = button.dataset.videoId; const video = document.querySelector( `.liziaVideo[data-video-id="${videoId}"]`, ); if (video) { toggleMute(video); } }); }); // Event listeners pour cliquer sur le container vidéo videoContainers.forEach((container) => { container.addEventListener("click", (e) => { // Ne pas déclencher si on clique sur les boutons if ( e.target.closest(".videoPlayBtn") || e.target.closest(".videoMuteBtn") ) { return; } const video = container.querySelector(".liziaVideo"); if (video) { togglePlay(video); } }); }); } // Initialiser les contrôles vidéo initVideoControls(); // Forcer le chargement du premier frame de la vidéo pour afficher le poster/thumbnail function loadVideoPosters() { const videos = document.querySelectorAll(".liziaVideo"); videos.forEach((video) => { // Supprimer l'attribut poster pour utiliser la première frame video.removeAttribute("poster"); // Forcer le chargement des métadonnées video.load(); // Charger le premier frame pour l'afficher comme poster const loadFirstFrame = () => { if (video.readyState >= 2) { // HAVE_CURRENT_DATA // Charger le premier frame en avançant légèrement puis en revenant video.currentTime = 0.1; video.addEventListener( "seeked", () => { video.currentTime = 0; video.pause(); }, { once: true }, ); } else { // Attendre que les métadonnées soient chargées video.addEventListener("loadeddata", loadFirstFrame, { once: true }); } }; // Charger la première frame sur tous les appareils if (video.readyState >= 1) { // HAVE_METADATA loadFirstFrame(); } else { video.addEventListener("loadedmetadata", loadFirstFrame, { once: true, }); } }); } // Charger les posters vidéo après un court délai pour laisser le DOM se charger setTimeout(() => { loadVideoPosters(); }, 300); // --- ANIMATIONS AU SCROLL --- function initScrollAnimations() { const isMobile = window.innerWidth <= 767; // Sur mobile, révéler immédiatement les vidéos pour éviter le fond blanc if (isMobile) { const videoCards = document.querySelectorAll( ".videoCard[data-scroll-reveal]", ); videoCards.forEach((card) => { card.classList.add("revealed"); }); } const observerOptions = { root: null, rootMargin: isMobile ? "0px" : "0px 0px -10% 0px", // Sur mobile, déclencher immédiatement threshold: isMobile ? 0.01 : 0.15, // Sur mobile, déclencher dès qu'un pixel est visible }; const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { // Ajouter la classe revealed pour déclencher l'animation entry.target.classList.add("revealed"); // Autoplay supprimé selon demande utilisateur // La vidéo ne se lance que au clic } }); }, observerOptions); // Observer tous les éléments avec l'attribut data-scroll-reveal const elementsToReveal = document.querySelectorAll("[data-scroll-reveal]"); elementsToReveal.forEach((element) => { // Ne pas observer les vidéos sur mobile car elles sont déjà révélées if (!isMobile || !element.classList.contains("videoCard")) { observer.observe(element); } }); } // Initialiser les animations au scroll initScrollAnimations(); // --- GESTION DU TOGGLE AVANT/APRÈS --- function initBeforeAfterToggle() { const toggleBtn = document.getElementById("lightToggleBtn"); const imageContainer = document.querySelector(".beforeAfterImageContainer"); const gridContainer = document.querySelector(".beforeAfterGrid"); if (!toggleBtn || !imageContainer) return; // Précharger les deux images pour un basculement instantané const imageOff = "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/lizia_func2_off_blue_V1.jpg?v=1692695272"; const imageOn = "https://cdn.shopify.com/s/files/1/0759/3184/4957/files/lizia_func2_blue_V2.jpg"; // Précharger les images const preloadOff = new Image(); preloadOff.src = imageOff; const preloadOn = new Image(); preloadOn.src = imageOn; // État initial: light ON (activé par défaut) let isLightOn = true; function updateImage() { if (isLightOn) { toggleBtn.classList.add("active"); if (imageContainer) { imageContainer.classList.add("light-on"); } if (gridContainer) { gridContainer.classList.add("light-on"); } } else { toggleBtn.classList.remove("active"); if (imageContainer) { imageContainer.classList.remove("light-on"); } if (gridContainer) { gridContainer.classList.remove("light-on"); } } } toggleBtn.addEventListener("click", () => { isLightOn = !isLightOn; updateImage(); }); // Initialiser l'état updateImage(); } // Initialiser le toggle initBeforeAfterToggle(); // --- LOGIQUE ACCORDÉON "COMMENT ÇA MARCHE" --- const accordionItems = document.querySelectorAll(".howItWorksItem"); accordionItems.forEach((item) => { item.addEventListener("click", () => { // Si l'élément est déjà actif, on ne fait rien if (item.classList.contains("active")) return; // Fermer tous les autres éléments accordionItems.forEach((otherItem) => { otherItem.classList.remove("active"); }); // Ouvrir l'élément cliqué item.classList.add("active"); }); }); // --- BANDEAU MÉDIAS INFINI --- const initInfiniteMediaBanner = () => { const banner = document.querySelector(".mediaBanner"); if (!banner) return; const wrapper = banner.querySelector(".mediaBannerWrapper"); if (!wrapper) return; const originalSlide = wrapper.querySelector(".mediaBannerSlide"); if (!originalSlide) return; // Variables let slideWidthValue = 0; let isDragging = false; let startX = 0; let currentTranslateX = 0; let hasMoved = false; // Set touch-action to allow vertical scroll natively banner.style.touchAction = "pan-y"; // Setup dimensions and clones const calculateAndSetup = () => { // Get width of the single slide containing all logos let slideWidth = originalSlide.getBoundingClientRect().width; if (!slideWidth) slideWidth = originalSlide.offsetWidth; if (!slideWidth) slideWidth = originalSlide.scrollWidth; if (!slideWidth) { requestAnimationFrame(calculateAndSetup); return; } const bannerWidth = banner.offsetWidth || window.innerWidth; // We need enough clones to cover the screen + buffer const slidesNeeded = Math.ceil((bannerWidth * 2) / slideWidth) + 1; const currentSlides = wrapper.querySelectorAll(".mediaBannerSlide").length; const clonesNeeded = Math.max(0, slidesNeeded - currentSlides); for (let i = 0; i < clonesNeeded; i++) { const clonedSlide = originalSlide.cloneNode(true); wrapper.appendChild(clonedSlide); } slideWidthValue = slideWidth; // Inject CSS Animation let styleElement = document.getElementById("mediaBannerAnimation"); if (!styleElement) { styleElement = document.createElement("style"); styleElement.id = "mediaBannerAnimation"; document.head.appendChild(styleElement); } const isMobile = window.innerWidth <= 767; const animationDuration = isMobile ? "20s" : "40s"; // Define animation to move exactly one slide width styleElement.textContent = ` @keyframes slideMediaInfinite { 0% { transform: translateX(0); } 100% { transform: translateX(-${slideWidthValue}px); } } .mediaBannerWrapper { animation: slideMediaInfinite ${animationDuration} linear infinite; display: flex; /* Ensure slides are side by side */ width: max-content; /* Ensure wrapper takes full width of content */ } .mediaBannerWrapper.dragging { animation: none !important; /* Stop animation during drag */ } `; }; // --- Event Handlers --- const handleStart = (clientX) => { isDragging = true; hasMoved = false; startX = clientX; // Get current position to resume/drag from there const computedStyle = window.getComputedStyle(wrapper); const matrix = computedStyle.transform; if (matrix && matrix !== "none") { const values = matrix.match(/matrix.*\((.+)\)/); if (values) { const matrixValues = values[1].split(", "); currentTranslateX = parseFloat(matrixValues[4]) || 0; } } else { currentTranslateX = 0; } wrapper.classList.add("dragging"); // Stops CSS animation wrapper.style.transform = `translateX(${currentTranslateX}px)`; // Freeze at current pos wrapper.style.cursor = "grabbing"; wrapper.style.userSelect = "none"; }; const handleMove = (clientX, e) => { if (!isDragging) return; const dx = clientX - startX; // Threshold for click vs drag if (Math.abs(dx) > 5) { hasMoved = true; const links = wrapper.querySelectorAll("a"); links.forEach((link) => (link.style.pointerEvents = "none")); } // Move let newPos = currentTranslateX + dx; // Normalize (Infinite Loop Logic for Drag) if (slideWidthValue > 0) { while (newPos > 0) newPos -= slideWidthValue; while (newPos <= -slideWidthValue) newPos += slideWidthValue; } wrapper.style.transform = `translateX(${newPos}px)`; }; const handleEnd = () => { if (!isDragging) return; isDragging = false; wrapper.style.cursor = ""; wrapper.style.userSelect = ""; // Re-enable links setTimeout(() => { const links = wrapper.querySelectorAll("a"); links.forEach((link) => (link.style.pointerEvents = "")); }, 50); // Calculate progress and set negative delay to resume // This tricks the CSS animation to start from the current position if (slideWidthValue > 0) { // Get the current transform value from the style (set during drag) // We need to parse it back because currentTranslateX might be stale if we didn't update it in handleMove? // No, handleMove updates wrapper.style.transform directly but doesn't update currentTranslateX global? // Wait, handleMove DOES NOT update currentTranslateX global in the last version I saw? // Let's check handleMove again. // Actually, handleMove uses `let newPos` and sets style. // It DOES NOT update `currentTranslateX`. // So `currentTranslateX` is still the start position! // I need to read the current transform from the wrapper style. const currentTransform = wrapper.style.transform; const match = currentTransform.match(/translateX\(([^)]+)px\)/); if (match) { const currentPos = parseFloat(match[1]); const progress = currentPos / slideWidthValue; const delay = progress * 40; // 40s duration wrapper.style.animationDelay = `${delay}s`; } } // Reset to CSS animation wrapper.classList.remove("dragging"); wrapper.style.transform = ""; }; // Listeners banner.addEventListener("mousedown", (e) => { if (e.target.closest("a")) return; // Let links work if not dragging e.preventDefault(); handleStart(e.clientX); }); document.addEventListener("mousemove", (e) => { handleMove(e.clientX, e); }); document.addEventListener("mouseup", handleEnd); banner.addEventListener( "touchstart", (e) => { handleStart(e.touches[0].clientX); }, { passive: false }, ); document.addEventListener( "touchmove", (e) => { handleMove(e.touches[0].clientX, e); }, { passive: false }, ); document.addEventListener("touchend", handleEnd); // Init requestAnimationFrame(calculateAndSetup); window.addEventListener("resize", () => { requestAnimationFrame(calculateAndSetup); }); }; // Initialiser le bandeau médias initInfiniteMediaBanner(); // --- LOGIQUE SECTION ENGAGEMENTS (VALUES) --- const valueItems = document.querySelectorAll(".valueItem"); const detailTitle = document.getElementById("detailTitle"); const detailDesc = document.getElementById("detailDesc"); const valuesSubtitle = document.getElementById("valuesSubtitle"); const updateMobileSubtitle = (item) => { if (valuesSubtitle && window.innerWidth <= 900) { const desc = item.getAttribute("data-desc"); valuesSubtitle.textContent = desc; } }; valueItems.forEach((item) => { item.addEventListener("click", () => { // Unify logic: Always set active class (works for both desktop and mobile now) valueItems.forEach((i) => i.classList.remove("active")); item.classList.add("active"); // --- Desktop Logic --- if (window.innerWidth > 900 && detailTitle && detailDesc) { const title = item.getAttribute("data-title"); const desc = item.getAttribute("data-desc"); detailTitle.textContent = title; detailDesc.textContent = desc; } // --- Mobile Logic --- // Update the subtitle with the full description updateMobileSubtitle(item); }); }); // Default State: Active first item if (valueItems.length > 0) { // Ensure first item is active on load for both valueItems.forEach((i) => i.classList.remove("active")); valueItems[0].classList.add("active"); // On mobile, also update the subtitle immediately if (window.innerWidth <= 900) { updateMobileSubtitle(valueItems[0]); } } // --- NAVIGATION AVIS (REVIEWS) --- function initReviewsNavigation() { const container = document.getElementById("reviewsContainer"); const prevBtn = document.querySelector(".reviewsNavBtn.prev"); const nextBtn = document.querySelector(".reviewsNavBtn.next"); if (!container || !prevBtn || !nextBtn) return; const scrollAmount = 360 + 32; // Card width + gap prevBtn.addEventListener("click", () => { container.scrollBy({ left: -scrollAmount, behavior: "smooth", }); }); nextBtn.addEventListener("click", () => { container.scrollBy({ left: scrollAmount, behavior: "smooth", }); }); } initReviewsNavigation(); // --- SCROLL TO REVIEWS --- const reviewsSummary = document.querySelector(".reviewsSummary"); const reviewsSection = document.getElementById("reviewsSection"); if (reviewsSummary && reviewsSection) { reviewsSummary.addEventListener("click", () => { reviewsSection.scrollIntoView({ behavior: "smooth" }); }); } }); /script>