feishin/src/shared/components/image/image.tsx

125 lines
4.2 KiB
TypeScript
Raw Normal View History

import clsx from 'clsx';
import { motion, MotionConfigProps } from 'motion/react';
import { type ImgHTMLAttributes } from 'react';
import { Img } from 'react-image';
import { InView } from 'react-intersection-observer';
import styles from './image.module.css';
import { animationProps } from '/@/shared/components/animations/animation-props';
import { Icon } from '/@/shared/components/icon/icon';
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
interface ImageContainerProps extends MotionConfigProps {
children: React.ReactNode;
className?: string;
enableAnimation?: boolean;
}
interface ImageLoaderProps {
className?: string;
}
interface ImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 'src'> {
containerClassName?: string;
enableAnimation?: boolean;
imageContainerProps?: Omit<ImageContainerProps, 'children'>;
includeLoader?: boolean;
includeUnloader?: boolean;
src: string | string[] | undefined;
thumbHash?: string;
}
interface ImageUnloaderProps {
className?: string;
}
const FALLBACK_SVG =
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMDAiIGhlaWdodD0iMzAwIj48ZmlsdGVyIGlkPSJhIiB4PSIwIiB5PSIwIj48ZmVUdXJidWxlbmNlIHR5cGU9ImZyYWN0YWxOb2lzZSIgYmFzZUZyZXF1ZW5jeT0iLjc1IiBzdGl0Y2hUaWxlcz0ic3RpdGNoIi8+PGZlQ29sb3JNYXRyaXggdHlwZT0ic2F0dXJhdGUiIHZhbHVlcz0iMCIvPjwvZmlsdGVyPjxwYXRoIGZpbHRlcj0idXJsKCNhKSIgb3BhY2l0eT0iLjA1IiBkPSJNMCAwaDMwMHYzMDBIMHoiLz48L3N2Zz4=';
export function Image({
className,
containerClassName,
enableAnimation,
imageContainerProps,
includeLoader = true,
includeUnloader = true,
src,
}: ImageProps) {
if (src) {
return (
<InView>
{({ inView, ref }) => (
<div ref={ref}>
<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} />
</ImageContainer>
) : null
}
/>
</div>
)}
</InView>
);
}
return <ImageUnloader />;
}
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}>
{children}
</div>
);
}
return (
<motion.div
className={clsx(styles.imageContainer, className)}
{...animationProps.fadeIn}
{...props}
>
{children}
</motion.div>
);
}
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} />
</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" />
</div>
);
}