Page produits 360 style 02 - Bootstrap 5

🏷️ Extraits & Composants HTML 📅 30/03/2026 15:00:00 👤 Mezgani said
Bootstrap Bootstrap5 Dashboard Admin Widgets Html

Template Bootstrap 5 de page produits 360 avec différents widgets et graphiques.

<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="UTF-8" />
  <meta name="copyright" content="MEZGANI Said" />
  <meta name="author" content="AngularForAll" />
  <meta name="robots" content="noindex, nofollow" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Product Detail 360 Bootstrap 5 02 | AngularForAll</title>
<!-- Bootstrap 5 + Icons -->
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">

  <!-- Lightbox pour galerie photos -->
  <link href="https://cdn.jsdelivr.net/npm/lightbox2@2.11.4/dist/css/lightbox.min.css" rel="stylesheet">

  <!-- Google Fonts -->
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">

  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }

    body {
      font-family: 'Inter', sans-serif;
      background: linear-gradient(135deg, #f5f7fc 0%, #eef1f7 100%);
      color: #1e293b;
    }

    /* Header */
    .navbar-product {
      background: rgba(255,255,255,0.9);
      backdrop-filter: blur(10px);
      box-shadow: 0 4px 20px rgba(0,0,0,0.03);
      padding: 1rem 2rem;
      border-bottom: 1px solid rgba(0,0,0,0.05);
      position: sticky;
      top: 0;
      z-index: 100;
    }

    .breadcrumb {
      margin: 0;
      font-size: 0.9rem;
    }

    .breadcrumb a {
      color: #64748b;
      text-decoration: none;
    }

    /* Layout */
    .product-container {
      max-width: 1400px;
      margin: 2rem auto;
      padding: 0 1.5rem;
    }

    /* Section Viewer (3D + Photos) */
    .viewer-section {
      background: white;
      border-radius: 32px;
      padding: 24px;
      box-shadow: 0 20px 40px -12px rgba(0,0,0,0.1);
      margin-bottom: 30px;
    }

    /* Tabs 3D / Photos */
    .viewer-tabs {
      display: flex;
      gap: 12px;
      margin-bottom: 20px;
      border-bottom: 2px solid #eef2f6;
      padding-bottom: 12px;
    }

    .viewer-tab {
      padding: 10px 24px;
      border-radius: 30px;
      font-weight: 600;
      cursor: pointer;
      transition: all 0.2s;
      color: #64748b;
      background: transparent;
      border: none;
    }

    .viewer-tab.active {
      background: #0f172a;
      color: white;
    }

    .viewer-tab i {
      margin-right: 8px;
    }

    /* Conteneur 3D */
    #viewer-3d-container {
      display: block;
      position: relative;
    }

    #viewer-3d-container.hidden {
      display: none;
    }

    #canvas-wrapper {
      width: 100%;
      aspect-ratio: 1 / 1;
      border-radius: 24px;
      overflow: hidden;
      background: linear-gradient(145deg, #0f172a, #1e293b);
      box-shadow: inset 0 0 30px #00000040, 0 10px 30px rgba(0,0,0,0.2);
    }

    canvas {
      display: block;
      width: 100%;
      height: 100%;
      outline: none;
    }

    .viewer-badge {
      position: absolute;
      bottom: 20px;
      left: 20px;
      background: rgba(20,30,50,0.8);
      backdrop-filter: blur(12px);
      color: white;
      padding: 6px 18px;
      border-radius: 40px;
      font-size: 0.8rem;
      border: 1px solid rgba(255,255,255,0.2);
      z-index: 5;
    }

    .rotate-hint {
      position: absolute;
      bottom: 20px;
      right: 20px;
      background: rgba(0,0,0,0.5);
      backdrop-filter: blur(8px);
      color: #ccd6f6;
      padding: 6px 16px;
      border-radius: 30px;
      font-size: 0.8rem;
      z-index: 5;
    }

    /* Conteneur Photos */
    #photos-container {
      display: none;
    }

    #photos-container.active {
      display: block;
    }

    .photo-gallery {
      display: grid;
      grid-template-columns: 2fr 1fr 1fr;
      gap: 16px;
    }

    .gallery-main {
      grid-row: span 2;
      border-radius: 20px;
      overflow: hidden;
      box-shadow: 0 10px 30px rgba(0,0,0,0.1);
    }

    .gallery-main img {
      width: 100%;
      height: 100%;
      object-fit: cover;
      transition: transform 0.3s;
    }

    .gallery-main img:hover {
      transform: scale(1.02);
    }

    .gallery-thumb {
      border-radius: 20px;
      overflow: hidden;
      box-shadow: 0 5px 15px rgba(0,0,0,0.1);
      cursor: pointer;
    }

    .gallery-thumb img {
      width: 100%;
      height: 100%;
      object-fit: cover;
      transition: transform 0.3s;
    }

    .gallery-thumb img:hover {
      transform: scale(1.05);
    }

    /* Infos produit */
    .product-title {
      font-size: 2.5rem;
      font-weight: 700;
      letter-spacing: -0.02em;
      margin-bottom: 0.5rem;
    }

    .product-ref {
      color: #64748b;
      font-size: 0.9rem;
      margin-bottom: 1rem;
    }

    .price-section {
      display: flex;
      align-items: baseline;
      gap: 15px;
      margin: 20px 0;
    }

    .current-price {
      font-size: 2.5rem;
      font-weight: 700;
      color: #0f172a;
    }

    .old-price {
      font-size: 1.3rem;
      color: #94a3b8;
      text-decoration: line-through;
    }

    .discount-badge {
      background: #ef4444;
      color: white;
      padding: 4px 12px;
      border-radius: 30px;
      font-weight: 600;
      font-size: 0.9rem;
    }

    .stock-status {
      display: inline-flex;
      align-items: center;
      background: #dcfce7;
      color: #15803d;
      padding: 6px 16px;
      border-radius: 30px;
      font-weight: 500;
      margin-bottom: 20px;
    }

    /* Sélecteurs */
    .selector-section {
      margin: 25px 0;
    }

    .selector-title {
      font-weight: 600;
      margin-bottom: 12px;
      color: #334155;
    }

    .color-options {
      display: flex;
      gap: 16px;
      flex-wrap: wrap;
    }

    .color-option {
      cursor: pointer;
      text-align: center;
      transition: all 0.2s;
    }

    .color-dot {
      width: 48px;
      height: 48px;
      border-radius: 48px;
      border: 3px solid transparent;
      box-shadow: 0 4px 10px rgba(0,0,0,0.1);
      margin-bottom: 6px;
    }

    .color-option.active .color-dot {
      border-color: #0f172a;
      transform: scale(1.1);
    }

    .color-option span {
      font-size: 0.85rem;
      font-weight: 500;
    }

    .storage-options {
      display: flex;
      gap: 12px;
    }

    .storage-btn {
      padding: 12px 24px;
      border: 2px solid #e2e8f0;
      border-radius: 16px;
      background: white;
      font-weight: 600;
      transition: all 0.2s;
      cursor: pointer;
    }

    .storage-btn.active {
      border-color: #0f172a;
      background: #0f172a;
      color: white;
    }

    .storage-btn:hover {
      border-color: #0f172a;
    }

    /* Boutons action */
    .action-buttons {
      display: flex;
      gap: 16px;
      margin: 30px 0;
    }

    .btn-add-cart {
      flex: 1;
      background: #0f172a;
      color: white;
      border: none;
      padding: 18px 32px;
      border-radius: 60px;
      font-weight: 600;
      font-size: 1.1rem;
      transition: all 0.2s;
      box-shadow: 0 10px 25px -5px rgba(15,23,42,0.3);
    }

    .btn-add-cart:hover {
      background: #1e293b;
      transform: translateY(-2px);
      box-shadow: 0 15px 30px -5px rgba(15,23,42,0.4);
    }

    .btn-like {
      width: 60px;
      background: white;
      border: 2px solid #e2e8f0;
      border-radius: 60px;
      font-size: 1.5rem;
      color: #ef4444;
      transition: all 0.2s;
      cursor: pointer;
    }

    .btn-like:hover {
      background: #fef2f2;
      border-color: #fca5a5;
    }

    .btn-like.liked {
      background: #ef4444;
      color: white;
      border-color: #ef4444;
    }

    .btn-like.liked i {
      color: white;
    }

    /* Description */
    .description-box {
      background: white;
      border-radius: 24px;
      padding: 28px;
      margin-top: 24px;
      box-shadow: 0 8px 24px rgba(0,0,0,0.04);
    }

    .specs-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
      gap: 20px;
      margin-top: 20px;
    }

    .spec-item {
      text-align: center;
      padding: 16px;
      background: #f8fafc;
      border-radius: 16px;
    }

    .spec-item i {
      font-size: 2rem;
      color: #0f172a;
      margin-bottom: 10px;
    }

    /* Responsive */
    @media (max-width: 992px) {
      .product-container { padding: 0 1rem; }
      .product-title { font-size: 2rem; }
      .photo-gallery { grid-template-columns: 1fr 1fr; }
      .gallery-main { grid-column: span 2; }
    }

    @media (max-width: 768px) {
      .photo-gallery { grid-template-columns: 1fr; }
      .gallery-main { grid-column: span 1; }
      .action-buttons { flex-direction: column; }
      .btn-like { width: 100%; }
    }
  </style>

  <!-- Three.js -->
  <script type="importmap">
    {
      "imports": {
        "three": "https://unpkg.com/three@0.128.0/build/three.module.js",
        "three/addons/": "https://unpkg.com/three@0.128.0/examples/jsm/"
      }
    }
  </script>
</head>
<body>

<!-- Navigation -->
<nav class="navbar-product d-flex align-items-center justify-content-between">
  <div class="d-flex align-items-center gap-3">
    <i class="bi bi-shop fs-3" style="color: #0f172a;"></i>
    <span style="font-weight: 700; font-size: 1.3rem;">TechStore</span>
  </div>
  <nav aria-label="breadcrumb">
    <ol class="breadcrumb">
      <li class="breadcrumb-item"><a href="#">Accueil</a></li>
      <li class="breadcrumb-item"><a href="#">Smartphones</a></li>
      <li class="breadcrumb-item active">Smartphone X10</li>
    </ol>
  </nav>
</nav>

<!-- Contenu principal -->
<div class="product-container">
  <div class="row g-4">
    <!-- Colonne gauche : Visualiseur (3D + Photos) -->
    <div class="col-lg-7">
      <div class="viewer-section">
        <!-- Tabs -->
        <div class="viewer-tabs">
          <button class="viewer-tab active" onclick="switchViewer('3d')">
            <i class="bi bi-badge-3d"></i> Vue 3D 360°
          </button>
          <button class="viewer-tab" onclick="switchViewer('photos')">
            <i class="bi bi-images"></i> Photos
          </button>
        </div>

        <!-- Viewer 3D -->
        <div id="viewer-3d-container">
          <div id="canvas-wrapper">
            <canvas id="phone-canvas"></canvas>
          </div>
          <div class="viewer-badge">
            <i class="bi bi-arrow-repeat me-1"></i> Faites glisser pour tourner à 360°
          </div>
          <div class="rotate-hint">
            <i class="bi bi-mouse me-1"></i> Zoom avec la molette
          </div>
        </div>

        <!-- Viewer Photos -->
        <div id="photos-container">
          <div class="photo-gallery">
            <!-- Photo principale -->
            <div class="gallery-main">
              <a href="public/mobile.png" data-lightbox="product-gallery" data-title="Smartphone X10 - Vue face">
                <img src="public/mobile.png" alt="Smartphone face">
              </a>
            </div>

            <!-- Photos secondaires -->
            <div class="gallery-thumb">
              <a href="public/mobile.png" data-lightbox="product-gallery" data-title="Smartphone X10 - Vue arrière">
                <img src="public/mobile.png" alt="Vue arrière">
              </a>
            </div>

            <div class="gallery-thumb">
              <a href="public/mobile.png" data-lightbox="product-gallery" data-title="Smartphone X10 - Vue côté">
                <img src="public/mobile.png" alt="Vue côté">
              </a>
            </div>

            <div class="gallery-thumb">
              <a href="public/mobile.png" data-lightbox="product-gallery" data-title="Smartphone X10 - Détail caméra">
                <img src="public/mobile.png" alt="Détail caméra">
              </a>
            </div>

            <div class="gallery-thumb">
              <a href="public/mobile.png" data-lightbox="product-gallery" data-title="Smartphone X10 - Accessoires">
                <img src="public/mobile.png" alt="Accessoires">
              </a>
            </div>
          </div>

          <!-- Note : Remplacez les URLs ci-dessus par vos propres photos -->
          <div class="alert alert-info mt-3 mb-0">
            <i class="bi bi-info-circle me-2"></i>
            <strong>Personnalisation :</strong> Remplacez les URLs des images dans le code par vos propres photos (ex: https://votresite.com/photo.jpg)
          </div>
        </div>

        <!-- Miniatures couleurs rapide -->
        <div class="d-flex gap-3 mt-3 justify-content-center">
          <div class="color-option active" onclick="setPhoneColor(0x2c2c36, this)">
            <div class="color-dot" style="background: #2c2c36;"></div>
            <span>Noir</span>
          </div>
          <div class="color-option" onclick="setPhoneColor(0xc0b7a8, this)">
            <div class="color-dot" style="background: #c0b7a8;"></div>
            <span>Argent</span>
          </div>
          <div class="color-option" onclick="setPhoneColor(0x4a6fa5, this)">
            <div class="color-dot" style="background: #4a6fa5;"></div>
            <span>Bleu</span>
          </div>
        </div>
      </div>
    </div>

    <!-- Colonne droite : Infos produit -->
    <div class="col-lg-5">
      <div class="ps-lg-3">
        <!-- Titre et référence -->
        <h1 class="product-title">Smartphone X10 5G</h1>
        <div class="product-ref">
          <i class="bi bi-upc-scan me-1"></i> Réf: SM-X10-256-BK ·
          <span class="text-warning ms-2">
            <i class="bi bi-star-fill"></i> 4.7 (156 avis)
          </span>
        </div>

        <!-- Prix -->
        <div class="price-section">
          <span class="current-price">749 €</span>
          <span class="old-price">899 €</span>
          <span class="discount-badge">-17%</span>
        </div>

        <!-- Stock -->
        <div class="stock-status">
          <i class="bi bi-check-circle-fill me-2"></i>
          En stock · Livraison 24-48h
        </div>

        <!-- Sélecteur Couleur -->
        <div class="selector-section">
          <div class="selector-title">
            <i class="bi bi-palette me-2"></i>Couleur
          </div>
          <div class="color-options">
            <div class="color-option active" onclick="setPhoneColor(0x2c2c36, this); updateMainPhoto('noir')">
              <div class="color-dot" style="background: #2c2c36;"></div>
              <span>Noir Graphite</span>
            </div>
            <div class="color-option" onclick="setPhoneColor(0xc0b7a8, this); updateMainPhoto('argent')">
              <div class="color-dot" style="background: #c0b7a8;"></div>
              <span>Argent Titane</span>
            </div>
            <div class="color-option" onclick="setPhoneColor(0x4a6fa5, this); updateMainPhoto('bleu')">
              <div class="color-dot" style="background: #4a6fa5;"></div>
              <span>Bleu Océan</span>
            </div>
          </div>
        </div>

        <!-- Sélecteur Stockage -->
        <div class="selector-section">
          <div class="selector-title">
            <i class="bi bi-memory me-2"></i>Stockage
          </div>
          <div class="storage-options">
            <div class="storage-btn active" onclick="selectStorage(this, 749)">128 Go</div>
            <div class="storage-btn" onclick="selectStorage(this, 869)">256 Go</div>
            <div class="storage-btn" onclick="selectStorage(this, 1029)">512 Go</div>
          </div>
        </div>

        <!-- Boutons d'action -->
        <div class="action-buttons">
          <button class="btn-add-cart" onclick="addToCart()">
            <i class="bi bi-cart-plus me-2"></i>Ajouter au panier
          </button>
          <button class="btn-like" id="likeButton" onclick="toggleLike()" aria-label="Ajouter aux favoris">
            <i class="bi bi-heart"></i>
          </button>
        </div>

        <!-- Livraison / Garantie -->
        <div class="d-flex gap-3 text-secondary small mb-4">
          <span><i class="bi bi-truck me-1"></i> Livraison offerte</span>
          <span><i class="bi bi-shield-check me-1"></i> Garantie 2 ans</span>
          <span><i class="bi bi-arrow-return-left me-1"></i> Retour gratuit</span>
        </div>

        <!-- Description -->
        <div class="description-box">
          <h5 class="fw-bold mb-3">
            <i class="bi bi-file-text me-2"></i>Description
          </h5>
          <p>Le Smartphone X10 5G offre une expérience fluide avec son processeur octa-core, son écran AMOLED 6.7" 120Hz et sa batterie 5000 mAh. Capturez des photos exceptionnelles avec le triple capteur 108MP et profitez de la charge rapide 67W.</p>

          <div class="specs-grid">
            <div class="spec-item">
              <i class="bi bi-cpu"></i>
              <div class="fw-bold">Snapdragon 8</div>
              <small>Gen 3</small>
            </div>
            <div class="spec-item">
              <i class="bi bi-battery-charging"></i>
              <div class="fw-bold">5000 mAh</div>
              <small>Charge 67W</small>
            </div>
            <div class="spec-item">
              <i class="bi bi-camera"></i>
              <div class="fw-bold">108 MP</div>
              <small>Triple caméra</small>
            </div>
            <div class="spec-item">
              <i class="bi bi-droplet-half"></i>
              <div class="fw-bold">IP68</div>
              <small>Étanche</small>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

<!-- Scripts -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lightbox2@2.11.4/dist/js/lightbox.min.js"></script>

<script type="module">
  // --- THREE.JS : Visualisation 3D 360° ---
  import * as THREE from 'three';
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

  let scene, camera, renderer, controls, phoneBodyMaterial, phoneGroup;
  const canvas = document.getElementById('phone-canvas');

  function initThree() {
    scene = new THREE.Scene();
    scene.background = new THREE.Color('#0f172a');

    camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
    camera.position.set(3, 2, 5);

    renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
    renderer.setSize(canvas.clientWidth, canvas.clientHeight);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;

    controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.05;
    controls.autoRotate = true;
    controls.autoRotateSpeed = 1.8;
    controls.enableZoom = true;
    controls.enablePan = false;
    controls.target.set(0, 0.5, 0);
    controls.maxPolarAngle = Math.PI / 1.8;
    controls.minDistance = 2.2;
    controls.maxDistance = 7.0;

    // Lumières
    scene.add(new THREE.AmbientLight(0x404060, 0.55));

    const mainLight = new THREE.DirectionalLight(0xfff5e6, 1.2);
    mainLight.position.set(4, 6, 3);
    mainLight.castShadow = true;
    mainLight.receiveShadow = true;
    mainLight.shadow.mapSize.width = 1024;
    mainLight.shadow.mapSize.height = 1024;
    scene.add(mainLight);

    const fill1 = new THREE.PointLight(0x4466cc, 0.7);
    fill1.position.set(-3, 2, 4);
    scene.add(fill1);

    const fill2 = new THREE.PointLight(0xccaa88, 0.5);
    fill2.position.set(2, 1, -5);
    scene.add(fill2);

    const backLight = new THREE.PointLight(0x6688aa, 0.4);
    backLight.position.set(-1, 1.5, -6);
    scene.add(backLight);

    // Sol
    const planeGeo = new THREE.CircleGeometry(4, 20);
    const planeMat = new THREE.MeshStandardMaterial({ color: 0x1a2332, roughness: 0.4, metalness: 0.1, side: THREE.DoubleSide });
    const plane = new THREE.Mesh(planeGeo, planeMat);
    plane.rotation.x = -Math.PI/2;
    plane.position.y = -0.01;
    plane.receiveShadow = true;
    scene.add(plane);

    // Grille
    const grid = new THREE.GridHelper(6, 20, 0x88aaff, 0x334466);
    grid.position.y = 0;
    scene.add(grid);

    // Construction du téléphone
    phoneGroup = new THREE.Group();

    const bodyMat = new THREE.MeshStandardMaterial({ color: 0x2c2c36, roughness: 0.25, metalness: 0.4 });
    phoneBodyMaterial = bodyMat;

    const mainBody = new THREE.Mesh(new THREE.BoxGeometry(1.6, 3.2, 0.32), bodyMat);
    mainBody.castShadow = true;
    mainBody.receiveShadow = true;
    mainBody.position.y = 0.5;
    phoneGroup.add(mainBody);

    // Écran
    const screenMat = new THREE.MeshStandardMaterial({
      color: 0x0a0e14,
      roughness: 0.15,
      emissive: new THREE.Color(0x112233),
      emissiveIntensity: 0.3
    });
    const screen = new THREE.Mesh(new THREE.BoxGeometry(1.45, 2.95, 0.05), screenMat);
    screen.castShadow = true;
    screen.position.set(0, 0.5, 0.165);
    phoneGroup.add(screen);

    // Cadre
    const frameMat = new THREE.MeshStandardMaterial({ color: 0xccccdd, roughness: 0.2, metalness: 0.8 });
    const frontFrame = new THREE.Mesh(new THREE.BoxGeometry(1.58, 3.18, 0.02), frameMat);
    frontFrame.position.set(0, 0.5, 0.19);
    phoneGroup.add(frontFrame);

    // Module caméra
    const camMod = new THREE.Mesh(new THREE.CylinderGeometry(0.25, 0.25, 0.04), new THREE.MeshStandardMaterial({ color: 0x333338 }));
    camMod.rotation.y = 0.3;
    camMod.position.set(0.3, 1.35, -0.18);
    phoneGroup.add(camMod);

    // Lentilles
    const lens1 = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.1, 0.03), new THREE.MeshStandardMaterial({ color: 0x111116 }));
    lens1.rotation.x = Math.PI/2;
    lens1.position.set(0.3, 1.35, -0.2);
    phoneGroup.add(lens1);

    scene.add(phoneGroup);

    // Animation
    function animate() {
      requestAnimationFrame(animate);
      controls.update();
      renderer.render(scene, camera);
    }
    animate();

    // Redimensionnement
    const resizeObserver = new ResizeObserver(() => {
      const wrapper = document.getElementById('canvas-wrapper');
      const w = wrapper.clientWidth;
      renderer.setSize(w, w);
      camera.aspect = 1;
      camera.updateProjectionMatrix();
    });
    resizeObserver.observe(document.getElementById('canvas-wrapper'));
  }

  initThree();

  // Fonction globale pour changer couleur 3D
  window.setPhoneColor = (hexColor, element) => {
    if (phoneBodyMaterial) {
      phoneBodyMaterial.color.setHex(hexColor);
    }

    // Mise à jour UI
    if (element) {
      document.querySelectorAll('.color-option').forEach(opt => opt.classList.remove('active'));
      element.classList.add('active');
    }
  };

  // Désactiver auto-rotate après interaction
  canvas.addEventListener('mousedown', () => controls.autoRotate = false);
  canvas.addEventListener('touchstart', () => controls.autoRotate = false);

  let idleTimer;
  window.addEventListener('mousemove', () => {
    controls.autoRotate = false;
    clearTimeout(idleTimer);
    idleTimer = setTimeout(() => controls.autoRotate = true, 4000);
  });
</script>

<script>
  // --- Fonctions UI ---

  // Switch entre 3D et Photos
  window.switchViewer = (mode) => {
    const container3D = document.getElementById('viewer-3d-container');
    const containerPhotos = document.getElementById('photos-container');
    const tabs = document.querySelectorAll('.viewer-tab');

    tabs.forEach(tab => tab.classList.remove('active'));

    if (mode === '3d') {
      container3D.classList.remove('hidden');
      containerPhotos.classList.remove('active');
      tabs[0].classList.add('active');

      // Redimensionner canvas
      setTimeout(() => {
        const wrapper = document.getElementById('canvas-wrapper');
        const w = wrapper.clientWidth;
        const canvas = document.getElementById('phone-canvas');
        const renderer = canvas.getContext('webgl2') || canvas.getContext('webgl');
        if (renderer) {
          canvas.style.width = w + 'px';
          canvas.style.height = w + 'px';
        }
      }, 50);
    } else {
      container3D.classList.add('hidden');
      containerPhotos.classList.add('active');
      tabs[1].classList.add('active');

      // Réinitialiser lightbox
      if (typeof lightbox !== 'undefined') {
        lightbox.option({
          'resizeDuration': 200,
          'wrapAround': true,
          'albumLabel': 'Image %1 sur %2'
        });
      }
    }
  };

  // Sélection stockage
  window.selectStorage = (element, price) => {
    document.querySelectorAll('.storage-btn').forEach(btn => btn.classList.remove('active'));
    element.classList.add('active');
    document.querySelector('.current-price').textContent = price + ' €';
  };

  // Ajouter au panier
  window.addToCart = () => {
    const color = document.querySelector('.color-option.active span')?.textContent || 'Noir';
    const storage = document.querySelector('.storage-btn.active')?.textContent || '128 Go';
    const price = document.querySelector('.current-price').textContent;

    alert(`✅ Ajouté au panier !\n\nSmartphone X10\nCouleur: ${color}\nStockage: ${storage}\nPrix: ${price}`);
  };

  // Like
  window.toggleLike = () => {
    const btn = document.getElementById('likeButton');
    const icon = btn.querySelector('i');

    btn.classList.toggle('liked');

    if (btn.classList.contains('liked')) {
      icon.classList.remove('bi-heart');
      icon.classList.add('bi-heart-fill');
    } else {
      icon.classList.remove('bi-heart-fill');
      icon.classList.add('bi-heart');
    }
  };

  // Mise à jour photo principale (simulation)
  window.updateMainPhoto = (color) => {
    // Cette fonction peut être étendue pour changer les photos selon la couleur
    console.log('Couleur sélectionnée :', color);
    // Exemple: changer l'image principale selon la couleur
    // document.querySelector('.gallery-main img').src = `https://votresite.com/phone-${color}.jpg`;
  };

  // Initialisation lightbox
  document.addEventListener('DOMContentLoaded', () => {
    if (typeof lightbox !== 'undefined') {
      lightbox.option({
        'resizeDuration': 200,
        'wrapAround': true,
        'albumLabel': 'Image %1 sur %2',
        'fadeDuration': 300
      });
    }
  });
</script>

<!-- Note d'utilisation -->
<div style="position: fixed; bottom: 20px; left: 20px; background: rgba(255,255,255,0.9); padding: 8px 16px; border-radius: 30px; font-size: 0.8rem; box-shadow: 0 4px 12px rgba(0,0,0,0.1); z-index: 1000;">
  <i class="bi bi-image me-1"></i>
  <strong>Personnalisez :</strong> Remplacez les URLs des photos dans le code HTML (lignes avec picsum.photos)
</div>

</body>
</html>

Ouvrir cet aperçu dans un nouvel onglet du navigateur

🔗 Ouvrir dans le navigateur