// dependencies import clsx from 'clsx'; import Link from 'next/link'; import { Blurhash } from 'react-blurhash'; import { useInViewport } from 'react-in-viewport'; import React, { useEffect, useMemo, useRef, useState } from 'react'; // cdn url const imgBase = process.env.CDN_URL || 'https://cdn.craftiq.ai'; /** * component image * * @param props */ const ComponentImage = (props = {}) => { // ref const measureRef = useRef(null); // in viewport const { inViewport } = useInViewport(measureRef); // width/height const [width, setWidth] = useState(props.width); const [error, setError] = useState(false); const [height, setHeight] = useState(props.height); const [imgLoaded, setImgLoaded] = useState(!!props.forceLoaded); const [mediaLoaded, setMediaLoaded] = useState(false); // memo const aspect = useMemo(() => { // check aspect if (props.aspect === true) { // automatically create aspect of image return props.image?.width && props.image?.height ? `${props.image.width}/${props.image.height}` : 1; } // if aspect is string or number return props.aspect; }, [props.aspect, props.url, props.image?.id]); const srcset = useMemo(() => { // check width if (!width || !height) return []; // url const baseUrl = props.url ? props.url : (props.image?.url ? `${imgBase}/${props.image.url}` : null); // check base url if (!baseUrl) return null; // new url const actualUrl = new URL(baseUrl); // add width/height actualUrl.searchParams.append('width', width); actualUrl.searchParams.append('height', height); actualUrl.searchParams.append('aspect_ratio', `${width}:${height}`); // check gravity let gravity; // set gravity if (props.gravity === 'top') gravity = 'north'; // gravity if (gravity) actualUrl.searchParams.append('crop_gravity', gravity); // create srcset const srcset = [ 2, 3, 4, ].map((scale) => { // create url string const url = new URL(actualUrl.toString()); // update width/height url.searchParams.set('width', parseInt(width * scale)); url.searchParams.set('height', parseInt(height * scale)); url.searchParams.set('aspect_ratio', `${parseInt(width * scale)}:${parseInt(height * scale)}`); // return url return { scale, url : url.toString(), width : width * scale, }; }); // return full srcset return [ { url : actualUrl.toString(), scale : 1, width, }, ...srcset, ]; }, [aspect, width, height, props.url, props.image?.id]); // use effect useEffect(() => { // if no onLoad if (!props.onLoad) return; // on load props.onLoad(imgLoaded || mediaLoaded); }, [imgLoaded, mediaLoaded]); useEffect(() => { // check height if (!measureRef?.current?.offsetHeight) return; // computed height const computedHeight = Math.floor(measureRef?.current?.offsetHeight); // if computed height if (isNaN(computedHeight)) return; // set height setHeight(computedHeight); }, [measureRef?.current?.offsetHeight]); useEffect(() => { // check width if (!measureRef?.current?.offsetWidth) return; // computed width const computedWidth = Math.floor(measureRef?.current?.offsetWidth); // if computed width if (isNaN(computedWidth)) return; // set width setWidth(computedWidth); }, [measureRef?.current?.offsetWidth]); // return jsx return ( {/* BLURHASH */} {!!(props.image?.blurhash && !props.noBlurhash) && (