Other - Product Media Gallery

Product Media Gallery

Example of combining multiple media elements Images, Video, 3D Viewer - into a single product gallery. All elements were created using Alter Product solutions.

Docs Other - Product Media Gallery embedding - live preview / embed demo (Vanilla JS).
1<!DOCTYPE html>
2<html>
3  <head>
4    <title>Docs Advanced - Product media gallery</title>
5    <meta charset="utf-8" />
6    <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
8    <link rel="stylesheet" href="./src/Styles.css">
9    <link href="https://vjs.zencdn.net/8.16.1/video-js.css" rel="stylesheet" />
10  </head>
11
12  <body>
13    <div id="wrapper">
14      <div id="displayWrapper"></div>
15      <div id="buttonsWrapper"></div>
16    </div>
17
18    <script src="./src/App.js"></script>
19    <script src="https://vjs.zencdn.net/8.16.1/video.js"></script>
20  </body>
21</html>
Vanilla JS - App.js
const responseApi = [
  {
    type: 'img',
    urlThumb: './assets/tshirt/black/image_0_thumb.png',
    urlContent: './assets/tshirt/black/image_0_big.png',
    alt: 'T-Shirt front',
  },
  {
    type: 'img',
    urlThumb: './assets/tshirt/black/image_1_thumb.png',
    urlContent: './assets/tshirt/black/image_1_big.png',
    alt: 'T-Shirt back',
  },
  {
    type: 'video',
    urlThumb: './assets/tshirt/black/image_2_thumb.png',
    urlContent: './assets/tshirt/black/image_0_big.png',
    alt: 'T-Shirt video thumbnail',
  },
  {
    type: 'viewer3D',
    urlThumb: './assets/tshirt/black/image_3_thumb.png',
    urlContent: './assets/tshirt/black/image_0_big.png',
    alt: 'T-Shirt 3D Viewer thumbnail',
  },
];

let selectedPreview = 0;

function initialiseProduct() {
  const buttonsWrapper = document.getElementById('buttonsWrapper');
  if (!buttonsWrapper) return;

  responseApi.forEach((item, index) => {
    const elementContainer = document.createElement('div');
    elementContainer.className = 'buttonContainer';

    if (index === selectedPreview) {
      displayMedia(index);
      elementContainer.classList.add('active');
    }

    buttonsWrapper.appendChild(elementContainer);

    elementContainer.addEventListener('click', function () {
      buttonClickHandler(this, index);
    });

    const elementThumb = document.createElement('img');
    elementThumb.className = 'imgThumb';
    elementThumb.src = item.urlThumb;
    elementThumb.alt = item.alt;
    elementContainer.appendChild(elementThumb);

    if (item.type === 'video') {
      const elementIcon = document.createElement('img');
      elementIcon.className = 'button-icon';
      elementIcon.src = './assets/icons/icon-play-video.svg';
      elementIcon.alt = 'Play video';
      elementContainer.appendChild(elementIcon);
    } else if (item.type === 'viewer3D') {
      const elementIcon = document.createElement('img');
      elementIcon.className = 'button-icon';
      elementIcon.src = './assets/icons/icon-3d.svg';
      elementIcon.alt = 'Open 3D Viewer';
      elementContainer.appendChild(elementIcon);
    }
  });
}

function buttonClickHandler(element, index) {
  if (selectedPreview === index) return;
  selectedPreview = index;

  document.querySelectorAll('#buttonsWrapper .buttonContainer').forEach((el) => {
    el.classList.remove('active');
  });

  element.classList.add('active');
  displayMedia(index);
}

function adjustVideoSize(player, container) {
  function updateSize() {
    const containerWidth = container.clientWidth;
    const containerHeight = container.clientHeight;
    const containerRatio = containerWidth / containerHeight;

    const videoWidth = player.videoWidth();
    const videoHeight = player.videoHeight();
    const videoRatio = videoWidth / videoHeight;

    if (videoRatio > containerRatio) {
      player.width(containerWidth);
      player.height(containerWidth / videoRatio);
    } else {
      player.height(containerHeight);
      player.width(containerHeight * videoRatio);
    }
  }

  player.on('loadedmetadata', updateSize);
  window.addEventListener('resize', updateSize);
}

function displayMedia(index) {
  const displayWrapper = document.getElementById('displayWrapper');
  if (!displayWrapper) return;

  const { type, urlContent, alt } = responseApi[index];

  if (type === 'img') {
    const img = document.createElement('img');
    img.src = urlContent;
    img.alt = alt || '';
    displayWrapper.replaceChildren(img);
    return;
  }

  if (type === 'video') {
    const video = document.createElement('video');
    video.id = 'my-video';
    video.className = 'video-js';
    video.loop = true;
    video.autoplay = true;
    video.muted = true;
    video.preload = 'auto';
    video.setAttribute('playsinline', '');

    const source = document.createElement('source');
    source.src = './assets/tshirt/black/product-media-gallery-loop-embeding.webm';
    source.type = 'video/webm';

    const noJsMessage = document.createElement('p');
    noJsMessage.className = 'vjs-no-js';
    noJsMessage.textContent =
      'To view this video please enable JavaScript and use a browser that supports HTML5 video.';

    video.appendChild(source);
    video.appendChild(noJsMessage);

    displayWrapper.replaceChildren(video);

    if (window.videojs) {
      const player = window.videojs(video);
      adjustVideoSize(player, displayWrapper);
    } else {
      console.warn('Video.js is not loaded yet!');
    }
    return;
  }

  if (type === 'viewer3D') {
    const iframe = document.createElement('iframe');
    iframe.loading = 'lazy';
    iframe.title = 'Product Viewer';
    iframe.src = 'https://alterproduct.com/app/viewer/1';
    iframe.width = '100%';
    iframe.height = '100%';
    iframe.frameBorder = '0';
    iframe.allowFullscreen = true;

    // hardening (opcjonalnie): iframe sandbox też możesz dodać, jeśli to embed zewnętrzny
    // iframe.sandbox = 'allow-scripts allow-same-origin allow-forms allow-popups';

    displayWrapper.replaceChildren(iframe);
  }
}

initialiseProduct();
Vanilla JS - Styles.css
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html,
body {
  width: 100%;
  height: 100%;
  display: grid;
  justify-content: center;
  align-items: center;
}

#wrapper {
  display: grid;
  min-height: 400px;
  grid-template-rows: minmax(340px, 50vh) auto;
  grid-template-columns: 1fr;
  gap: 20px;
  user-select: none;
  margin: 10px;
  max-width: 700px;
}

#displayWrapper {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
}

@media only screen and (max-width: 720px) {
  #displayWrapper {
    width: calc(100vw - 40px);
  }
}

#displayWrapper img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

#buttonsWrapper {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: 1fr;
}

@media only screen and (max-width: 600px) {
  #buttonsWrapper {
    grid-template-columns: repeat(3, 1fr);
  }
  #wrapper {
    gap: 10px;
  }
}

@media only screen and (max-width: 450px) {
  #buttonsWrapper {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media only screen and (max-width: 300px) {
  #buttonsWrapper {
    grid-template-columns: repeat(1, 150px);
    grid-auto-rows: 150px;
    justify-content: center;
  }
}

.buttonContainer {
  cursor: pointer;
  transition: 0.3s;
  position: relative;
  overflow: hidden;
}

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

.button-icon {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 120px !important;
  height: 120px !important;
  object-fit: contain;
  transform: translate(-50%, -50%);
  filter: drop-shadow(0 0 6px rgba(0, 0, 0, 0.5));
}

.buttonContainer:hover .imgThumb {
  transform: scale(1.2);
}

.active {
  cursor: auto;
  box-shadow: inset 0 0 0 1px rgb(129, 129, 129);
}

.active:hover .imgThumb {
  transform: scale(1);
}

/* Video */
video,
.video-js {
  background: white !important;
  outline: none;
  border: none;
}

.vjs-poster {
  background: transparent !important;
}

video:focus {
  outline: none;
}
React - AdvancedProductMediaGallery.js
import React, { useState } from 'react';
import './Styles.css';
import { responseApi } from './responseApi';

import MediaButtons from './MediaButtons';
import DisplayMedia from './DisplayMedia';

const AdvancedProductMediaGallery = () => {
  const [selectedPreview, setSelectedPreview] = useState(0);

  return (
    <div id="wrapper">
      <DisplayMedia responseApi={responseApi} selectedPreview={selectedPreview} />
      <MediaButtons
        responseApi={responseApi}
        selectedPreview={selectedPreview}
        setSelectedPreview={setSelectedPreview}
      />
    </div>
  );
};

export default AdvancedProductMediaGallery;
React - DisplayMedia.js
import React from 'react';
import VideoComponent from './VideoComponent';

const DisplayMedia = ({ responseApi, selectedPreview }) => {
  const item = responseApi[selectedPreview];

  return (
    <div id="displayWrapper">
      {item.type === 'img' ? (
        <img src={item.urlContent} alt={item.alt} />
      ) : null}

      {item.type === 'video' ? (
        <VideoComponent />
      ) : null}

      {item.type === 'viewer3D' ? (
        <iframe
          loading="lazy"
          title="Product Viewer"
          src="https://alterproduct.com/app/viewer/1"
          width="100%"
          height="100%"
          frameBorder="0"
          allowFullScreen
        />
      ) : null}
    </div>
  );
};

export default DisplayMedia;
React - MediaButtons.js
const MediaButtons = ({ responseApi, selectedPreview, setSelectedPreview }) => {
  return (
    <div id="buttonsWrapper">
      {responseApi.map((item, index) => (
        <button
          key={'buttonMedia' + index}
          type="button"
          className={'buttonContainer ' + (selectedPreview === index ? 'active' : '')}
          onClick={() => setSelectedPreview(index)}
        >
          <img src={item.urlThumb} alt={item.alt} className="imgThumb" />

          {item.type === 'video' ? (
            <img src={'/assets/icons/icon-play-video.svg'} alt={'Play video'} className="button-icon" />
          ) : null}

          {item.type === 'viewer3D' ? (
            <img src={'/assets/icons/icon-3d.svg'} alt={'Open 3D Viewer'} className="button-icon" />
          ) : null}
        </button>
      ))}
    </div>
  );
};

export default MediaButtons;
React - responseApi.js
export const responseApi = [
  {
    type: 'img',
    urlThumb: '/assets/image/tshirt/black/image_0_thumb.png',
    urlContent: '/assets/image/tshirt/black/image_0_big.png',
    alt: 'T-Shirt front',
  },
  {
    type: 'img',
    urlThumb: '/assets/image/tshirt/black/image_1_thumb.png',
    urlContent: '/assets/image/tshirt/black/image_1_big.png',
    alt: 'T-Shirt back',
  },
  {
    type: 'video',
    urlThumb: '/assets/image/tshirt/black/image_2_thumb.png',
    urlContent: '/assets/image/tshirt/black/image_0_big.png',
    alt: 'T-Shirt video thumbnail',
  },
  {
    type: 'viewer3D',
    urlThumb: '/assets/image/tshirt/black/image_3_thumb.png',
    urlContent: '/assets/image/tshirt/black/image_0_big.png',
    alt: 'T-Shirt 3D Viewer thumbnail',
  },
];
React - Styles.css
/* reuse Styles.css from vanilla example */
Open the project on GitHubGitHub logo