Skip to content

Commit ecde83e

Browse files
Added multi-image support (#35)
* Added multi-image support * Addressing PR comments
1 parent 022bebb commit ecde83e

File tree

6 files changed

+136
-10
lines changed

6 files changed

+136
-10
lines changed

src/Product.scss

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,40 @@
2323

2424
#{$block}__imgcontainer {
2525
flex: 0 0 $imgSize;
26+
position: relative;
27+
28+
#{$block}__img {
29+
width: 100%;
30+
height: 100%;
31+
object-fit: contain;
32+
}
33+
34+
#{$block}__previmagebtn, #{$block}__nextimagebtn {
35+
position: absolute;
36+
color: hsl(0, 0, 30%);
37+
background-color: hsla(0, 0, 100%, 0.3);
38+
top: 50%;
39+
margin-top: -22px;
40+
width: 44px;
41+
height: 44px;
42+
cursor: pointer;
43+
44+
&:hover {
45+
background-color: hsla(0, 0, 100%, 0.6);
46+
}
47+
48+
&:focus {
49+
box-shadow: 0 0 0 1px hsl(0, 0, 80%);
50+
}
51+
}
52+
53+
#{$block}__previmagebtn {
54+
left: 10px;
55+
}
56+
57+
#{$block}__nextimagebtn {
58+
right: 10px;
59+
}
2660
}
2761

2862
#{$block}__details {

src/Product.tsx

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import React from 'react';
1+
import React, { useState } from 'react';
22
import { useParams } from 'react-router-dom';
3-
import { useResolve } from './hooks';
3+
import { useResolve, useProductImages } from './hooks';
44
import { loadProductBySlug } from './service';
55
import { CompareCheck } from './CompareCheck';
6-
import { ProductMainImage } from './ProductMainImage';
76
import { useTranslation, useCurrency } from './app-state';
87
import { isProductAvailable } from './helper';
98

@@ -18,17 +17,46 @@ export const Product: React.FC = () => {
1817
const { productSlug } = useParams<ProductParams>();
1918
const { t, selectedLanguage } = useTranslation();
2019
const { selectedCurrency } = useCurrency();
20+
2121
const [product] = useResolve(
2222
async () => loadProductBySlug(productSlug, selectedLanguage, selectedCurrency),
2323
[productSlug, selectedLanguage, selectedCurrency]
2424
);
2525

26+
const productImageHrefs = useProductImages(product);
27+
const [currentImageIndex, setCurrentImageIndex] = useState(0);
28+
const isPrevImageVisible = currentImageIndex > 0;
29+
const isNextImageVisible = currentImageIndex < (productImageHrefs?.length ?? 0) - 1;
30+
const productBackground = product?.background_color ?? '';
31+
32+
const handlePrevImageClicked = () => {
33+
setCurrentImageIndex(currentImageIndex - 1);
34+
};
35+
36+
const handleNextImageClicked = () => {
37+
setCurrentImageIndex(currentImageIndex + 1);
38+
};
39+
2640
return (
2741
<div className="product">
2842
{product && (
2943
<div className="product__maincontainer">
3044
<div className="product__imgcontainer">
31-
<ProductMainImage product={product} size={400} />
45+
{productImageHrefs.length > 0 && (
46+
<>
47+
<img className="product__img" src={productImageHrefs?.[currentImageIndex]} alt={product.name} style={{ backgroundColor: productBackground }} />
48+
{isPrevImageVisible && (
49+
<button className="product__previmagebtn" aria-label={t('previous-image')} onClick={handlePrevImageClicked}>
50+
<svg fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" stroke="currentColor"><path d="M15 19l-7-7 7-7"></path></svg>
51+
</button>
52+
)}
53+
{isNextImageVisible && (
54+
<button className="product__nextimagebtn" aria-label={t('next-image')} onClick={handleNextImageClicked}>
55+
<svg fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" stroke="currentColor"><path d="M9 5l7 7-7 7"></path></svg>
56+
</button>
57+
)}
58+
</>
59+
)}
3260
</div>
3361
<div className="product__details">
3462
<h1 className="product__name">

src/hooks.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { useEffect, useState } from 'react';
2+
import { Product, loadImageHref } from './service';
23

34
type FetchHookResult<R> = [
45
R | undefined,
@@ -34,3 +35,48 @@ export function useResolve<R>(promiseFn: () => Promise<R> | undefined, deps?: Re
3435

3536
return result;
3637
}
38+
39+
export function useProductImages(product: Product | undefined) {
40+
const [productImageHrefs, setProductImageHrefs] = useState<string[]>([]);
41+
42+
useEffect(() => {
43+
let isCurrent = true;
44+
setProductImageHrefs([]);
45+
46+
(async () => {
47+
// Load main image first so it can be presented right away and then load additional files one by one
48+
const result: string[] = [];
49+
const mainImageId = product?.relationships?.main_image?.data?.id;
50+
if (mainImageId) {
51+
const mainImageHref = await loadImageHref(mainImageId);
52+
53+
if (!isCurrent) {
54+
return;
55+
}
56+
57+
if (mainImageHref) {
58+
result.push(mainImageHref);
59+
setProductImageHrefs(result);
60+
}
61+
}
62+
63+
const files = product?.relationships?.files?.data ?? [];
64+
for (const file of files) {
65+
const imageHref = await loadImageHref(file.id);
66+
67+
if (!isCurrent) {
68+
return;
69+
}
70+
71+
if (imageHref) {
72+
result.push(imageHref);
73+
setProductImageHrefs([...result]);
74+
}
75+
}
76+
})();
77+
78+
return () => { isCurrent = false; };
79+
}, [product]);
80+
81+
return productImageHrefs;
82+
}

src/locales/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,5 +93,7 @@
9393
"password-confirmation": "Password Confirmation",
9494
"submit": "Submit",
9595
"required": "Required",
96-
"network-offline": "Network Offline"
96+
"network-offline": "Network Offline",
97+
"previous-image": "Previous image",
98+
"next-image": "Next image"
9799
}

src/locales/fr.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,5 +93,7 @@
9393
"password-confirmation": "ÞÞåššŵŵöŕð ÇÇöñƒƒîŕɱɱåţţîöñ",
9494
"submit": "ŠŠûƀɱɱîţ",
9595
"required": "ŔŔéǫûîîŕéð",
96-
"network-offline": "ÑÑéţŵŵöŕķ ÖÖƒƒļļîñé"
96+
"network-offline": "ÑÑéţŵŵöŕķ ÖÖƒƒļļîñé",
97+
"previous-image": "ÞÞŕéṽîîöûš îîɱååĝé",
98+
"next-image": "ÑÑéẋţ îîɱååĝé"
9799
}

src/service.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ export interface ProductBase {
6363
type: string;
6464
};
6565
};
66+
files: {
67+
data: {
68+
type: 'file';
69+
id: string;
70+
}[];
71+
};
6672
categories?: any;
6773
collections?: any;
6874
brands?: any;
@@ -182,18 +188,26 @@ export async function loadCategoryProducts(categoryId: string, pageNum: number,
182188

183189
const imageHrefCache: { [key: string]: string } = {};
184190

185-
export async function loadImageHref(imageId: string): Promise<string | undefined> {
186-
if (!imageId) {
187-
return undefined;
188-
}
191+
const imageMimeTypes = [
192+
'image/jpg',
193+
'image/jpeg',
194+
'image/png',
195+
'image/gif',
196+
];
189197

198+
// Loads a file with a provided id and returns its url if it's mime type is an image or undefined otherwise
199+
export async function loadImageHref(imageId: string): Promise<string | undefined> {
190200
if (imageHrefCache[imageId]) {
191201
return imageHrefCache[imageId];
192202
}
193203

194204
const moltin = MoltinGateway({ client_id: config.clientId });
195205
const result = await moltin.Files.Get(imageId);
196206

207+
if (imageMimeTypes.indexOf(result.data.mime_type) === -1) {
208+
return undefined;
209+
}
210+
197211
imageHrefCache[imageId] = result.data.link.href;
198212

199213
return result.data.link.href;

0 commit comments

Comments
 (0)