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.
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 */