(frontend): add image preloading hook and suspense image component

- Add `usePreloadImages` hook to preload profile pictures
- Add `SuspenseImageComponent` for handling image loading with suspense
- Update `ProfilePictureChooserComponent` to use the new hook and component
This commit is contained in:
cristhianzl 2024-06-10 17:23:11 -03:00
commit d028d83d37
3 changed files with 98 additions and 7 deletions

View file

@ -0,0 +1,42 @@
import { useEffect } from "react";
import {
BACKEND_URL,
BASE_URL_API,
} from "../../../../../../../../../constants/constants";
const usePreloadImages = (profilePictures, setImagesLoaded) => {
const preloadImages = async (imageUrls) => {
return Promise.all(
imageUrls.map(
(src) =>
new Promise((resolve) => {
const img = new Image();
img.src = src;
img.onload = resolve;
img.onerror = resolve;
}),
),
);
};
useEffect(() => {
const imageArray: string[] = [];
const firstUrl = `${BACKEND_URL.slice(0, BACKEND_URL.length - 1)}`;
Object.keys(profilePictures).flatMap((folder) =>
profilePictures[folder].map((path) =>
imageArray.push(
`${firstUrl}${BASE_URL_API}files/profile_pictures/${folder}/${path}`,
),
),
);
preloadImages(imageArray).then(() => {
setImagesLoaded(true);
});
}, [profilePictures]);
return;
};
export default usePreloadImages;

View file

@ -1,12 +1,13 @@
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import { Button } from "../../../../../../../../components/ui/button";
import Loading from "../../../../../../../../components/ui/loading";
import {
BACKEND_URL,
BASE_URL_API,
} from "../../../../../../../../constants/constants";
import Loading from "../../../../../../../../components/ui/loading";
import { cn } from "../../../../../../../../utils/utils";
import { Button } from "../../../../../../../../components/ui/button";
import { useDarkStore } from "../../../../../../../../stores/darkStore";
import { cn } from "../../../../../../../../utils/utils";
import usePreloadImages from "./hooks/use-preload-images";
type ProfilePictureChooserComponentProps = {
profilePictures: { [key: string]: string[] };
@ -23,6 +24,7 @@ export default function ProfilePictureChooserComponent({
}: ProfilePictureChooserComponentProps) {
const ref = useRef<HTMLButtonElement>(null);
const dark = useDarkStore((state) => state.dark);
const [imagesLoaded, setImagesLoaded] = useState(false);
useEffect(() => {
if (value && ref) {
@ -30,9 +32,11 @@ export default function ProfilePictureChooserComponent({
}
}, [ref, value]);
usePreloadImages(profilePictures, setImagesLoaded);
return (
<div className="flex flex-col justify-center gap-2">
{loading ? (
{loading || !imagesLoaded ? (
<Loading />
) : (
Object.keys(profilePictures).map((folder, idx) => (
@ -41,7 +45,7 @@ export default function ProfilePictureChooserComponent({
<span className="font-normal">{folder}</span>
</div>
<div className="block overflow-hidden">
<div className="flex items-center gap-1 overflow-x-auto rounded-lg bg-background px-1 custom-scroll">
<div className="flex items-center gap-1 overflow-x-auto rounded-lg bg-muted px-1 custom-scroll">
{profilePictures[folder].map((path, idx) => (
<Button
ref={value === folder + "/" + path ? ref : undefined}
@ -55,7 +59,9 @@ export default function ProfilePictureChooserComponent({
src={`${BACKEND_URL.slice(
0,
BACKEND_URL.length - 1,
)}${BASE_URL_API}files/profile_pictures/${folder + "/" + path}`}
)}${BASE_URL_API}files/profile_pictures/${
folder + "/" + path
}`}
style={{
filter:
value === folder + "/" + path

View file

@ -0,0 +1,43 @@
type SuspenseImageComponentProps = { src: string };
const imgCache = {
__cache: {},
read(src) {
if (!this.__cache[src]) {
this.__cache[src] = new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
this.__cache[src] = true;
resolve(true);
};
img.onerror = () => {
delete this.__cache[src]; // Remove failed cache entry
reject(new Error("Image failed to load"));
};
img.src = src;
});
}
if (this.__cache[src] instanceof Promise) {
throw this.__cache[src];
}
return this.__cache[src];
},
};
const SuspenseImageComponent = ({
src,
...rest
}: SuspenseImageComponentProps) => {
try {
imgCache.read(src);
} catch (promise) {
if (promise instanceof Promise) {
throw promise;
}
throw new Error("Unexpected error in image loading");
}
return <img src={src} {...rest} />;
};
export default SuspenseImageComponent;