2025-06-24 00:04:36 -07:00
|
|
|
import clsx from 'clsx';
|
2025-06-25 20:40:45 -07:00
|
|
|
import { motion, MotionConfigProps } from 'motion/react';
|
|
|
|
|
import { type ImgHTMLAttributes } from 'react';
|
2025-06-24 00:04:36 -07:00
|
|
|
import { Img } from 'react-image';
|
2025-07-07 20:11:32 -07:00
|
|
|
import { InView } from 'react-intersection-observer';
|
2025-06-24 00:04:36 -07:00
|
|
|
|
|
|
|
|
import styles from './image.module.css';
|
|
|
|
|
|
2025-06-25 20:40:45 -07:00
|
|
|
import { animationProps } from '/@/shared/components/animations/animation-props';
|
2025-06-24 00:04:36 -07:00
|
|
|
import { Icon } from '/@/shared/components/icon/icon';
|
|
|
|
|
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
|
|
|
|
|
2025-06-25 20:40:45 -07:00
|
|
|
interface ImageContainerProps extends MotionConfigProps {
|
2025-06-24 00:04:36 -07:00
|
|
|
children: React.ReactNode;
|
|
|
|
|
className?: string;
|
2025-06-25 20:40:45 -07:00
|
|
|
enableAnimation?: boolean;
|
2025-06-24 00:04:36 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ImageLoaderProps {
|
|
|
|
|
className?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 'src'> {
|
|
|
|
|
containerClassName?: string;
|
2025-06-25 20:40:45 -07:00
|
|
|
enableAnimation?: boolean;
|
|
|
|
|
imageContainerProps?: Omit<ImageContainerProps, 'children'>;
|
2025-06-24 00:04:36 -07:00
|
|
|
includeLoader?: boolean;
|
|
|
|
|
includeUnloader?: boolean;
|
|
|
|
|
src: string | string[] | undefined;
|
|
|
|
|
thumbHash?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ImageUnloaderProps {
|
|
|
|
|
className?: string;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-03 19:56:51 -07:00
|
|
|
const FALLBACK_SVG =
|
|
|
|
|
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMDAiIGhlaWdodD0iMzAwIj48ZmlsdGVyIGlkPSJhIiB4PSIwIiB5PSIwIj48ZmVUdXJidWxlbmNlIHR5cGU9ImZyYWN0YWxOb2lzZSIgYmFzZUZyZXF1ZW5jeT0iLjc1IiBzdGl0Y2hUaWxlcz0ic3RpdGNoIi8+PGZlQ29sb3JNYXRyaXggdHlwZT0ic2F0dXJhdGUiIHZhbHVlcz0iMCIvPjwvZmlsdGVyPjxwYXRoIGZpbHRlcj0idXJsKCNhKSIgb3BhY2l0eT0iLjA1IiBkPSJNMCAwaDMwMHYzMDBIMHoiLz48L3N2Zz4=';
|
|
|
|
|
|
2025-06-24 00:04:36 -07:00
|
|
|
export function Image({
|
|
|
|
|
className,
|
|
|
|
|
containerClassName,
|
2025-06-25 20:40:45 -07:00
|
|
|
enableAnimation,
|
|
|
|
|
imageContainerProps,
|
2025-06-24 00:04:36 -07:00
|
|
|
includeLoader = true,
|
|
|
|
|
includeUnloader = true,
|
|
|
|
|
src,
|
|
|
|
|
}: ImageProps) {
|
|
|
|
|
if (src) {
|
|
|
|
|
return (
|
2025-07-07 20:11:32 -07:00
|
|
|
<InView>
|
|
|
|
|
{({ inView, ref }) => (
|
|
|
|
|
<div ref={ref}>
|
2025-09-03 19:56:51 -07:00
|
|
|
<Img
|
|
|
|
|
className={clsx(styles.image, className)}
|
|
|
|
|
container={(children) => (
|
|
|
|
|
<ImageContainer
|
|
|
|
|
className={containerClassName}
|
|
|
|
|
enableAnimation={enableAnimation}
|
|
|
|
|
{...imageContainerProps}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</ImageContainer>
|
|
|
|
|
)}
|
|
|
|
|
loader={
|
|
|
|
|
includeLoader ? (
|
|
|
|
|
<ImageContainer className={containerClassName}>
|
|
|
|
|
<ImageLoader className={className} />
|
|
|
|
|
</ImageContainer>
|
|
|
|
|
) : null
|
|
|
|
|
}
|
|
|
|
|
src={inView ? src : FALLBACK_SVG}
|
|
|
|
|
unloader={
|
|
|
|
|
includeUnloader ? (
|
|
|
|
|
<ImageContainer className={containerClassName}>
|
|
|
|
|
<ImageUnloader className={className} />
|
2025-07-07 20:11:32 -07:00
|
|
|
</ImageContainer>
|
2025-09-03 19:56:51 -07:00
|
|
|
) : null
|
|
|
|
|
}
|
|
|
|
|
/>
|
2025-07-07 20:11:32 -07:00
|
|
|
</div>
|
2025-06-24 00:04:36 -07:00
|
|
|
)}
|
2025-07-07 20:11:32 -07:00
|
|
|
</InView>
|
2025-06-24 00:04:36 -07:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return <ImageUnloader />;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-25 20:40:45 -07:00
|
|
|
function ImageContainer({ children, className, enableAnimation, ...props }: ImageContainerProps) {
|
|
|
|
|
if (!enableAnimation) {
|
|
|
|
|
return (
|
2025-07-12 11:17:54 -07:00
|
|
|
<div className={clsx(styles.imageContainer, className)} {...props}>
|
2025-06-25 20:40:45 -07:00
|
|
|
{children}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<motion.div
|
|
|
|
|
className={clsx(styles.imageContainer, className)}
|
|
|
|
|
{...animationProps.fadeIn}
|
|
|
|
|
{...props}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</motion.div>
|
|
|
|
|
);
|
2025-06-24 00:04:36 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ImageLoader({ className }: ImageLoaderProps) {
|
|
|
|
|
return (
|
|
|
|
|
<div className={clsx(styles.loader, className)}>
|
2025-07-12 11:17:54 -07:00
|
|
|
<Skeleton className={clsx(styles.skeleton, className)} enableAnimation={true} />
|
2025-06-24 00:04:36 -07:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ImageUnloader({ className }: ImageUnloaderProps) {
|
|
|
|
|
return (
|
|
|
|
|
<div className={clsx(styles.unloader, className)}>
|
2025-07-12 11:17:54 -07:00
|
|
|
<Icon icon="emptyImage" size="xl" />
|
2025-06-24 00:04:36 -07:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|