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