✨ (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:
parent
986e67d389
commit
d028d83d37
3 changed files with 98 additions and 7 deletions
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue