fix: show loading component state while images arent fully loaded (#2609)

* ✨ (ProfilePictureForm): add loading state to handle initial loading state

* ♻️ (use-get-profile-pictures.ts): refactor profile pictures query to process data on the server side
♻️ (ProfilePictureForm): simplify state management by removing redundant loading state

* ♻️ (use-get-profile-pictures.ts): rename ProfilePicturesResponse to ProfilePicturesQueryResponse for clarity
♻️ (use-preload-images.tsx): add loading check to useEffect to prevent unnecessary execution
♻️ (profilePictureChooserComponent): update profilePictures prop type to handle undefined and add loading to usePreloadImages
♻️ (ProfilePictureForm): remove unnecessary state and use response directly from useGetProfilePicturesQuery

* 🐛 (use-preload-images.tsx): add missing dependency 'loading' to useEffect dependency array to ensure images are preloaded correctly
This commit is contained in:
Cristhian Zanforlin Lousa 2024-07-09 19:57:46 -03:00 • committed by GitHub
commit 7174e6ef7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 39 additions and 45 deletions

View file

@ -6,31 +6,46 @@ import { UseRequestProcessor } from "../../services/request-processor";
interface ProfilePicturesQueryParams {} interface ProfilePicturesQueryParams {}
export interface ProfilePicturesResponse { export interface ProfilePicturesQueryResponse {
files: string[]; files: string[];
} }
export const useGetProfilePicturesQuery: useQueryFunctionType< export const useGetProfilePicturesQuery: useQueryFunctionType<
ProfilePicturesQueryParams, ProfilePicturesQueryParams,
ProfilePicturesResponse { [key: string]: string[] }
> = () => { > = () => {
const { query } = UseRequestProcessor(); const { query } = UseRequestProcessor();
const getProfilePicturesFn = async () => { const getProfilePicturesFn =
const response = await api.get<ProfilePicturesResponse>( async (): Promise<ProfilePicturesQueryResponse> => {
`${getURL("FILES")}/profile_pictures/list`, const response = await api.get<ProfilePicturesQueryResponse>(
); `${getURL("FILES")}/profile_pictures/list`,
);
return response.data; return response.data;
};
const responseFn = async () => {
const data = await getProfilePicturesFn();
const profilePictures = {};
data?.files?.forEach((profile_picture) => {
const [folder, path] = profile_picture.split("/");
if (profilePictures[folder]) {
profilePictures[folder].push(path);
} else {
profilePictures[folder] = [path];
}
});
return profilePictures;
}; };
const queryResult = query( const queryResult = query(["useGetProfilePicturesQuery"], responseFn, {
["useGetProfilePicturesQuery"], placeholderData: keepPreviousData,
getProfilePicturesFn, });
{
placeholderData: keepPreviousData,
},
);
return queryResult; return queryResult;
}; };

View file

@ -4,6 +4,7 @@ import { BASE_URL_API } from "../../../../../../../../../constants/constants";
const usePreloadImages = ( const usePreloadImages = (
profilePictures: { [key: string]: string[] }, profilePictures: { [key: string]: string[] },
setImagesLoaded: (value: boolean) => void, setImagesLoaded: (value: boolean) => void,
loading: boolean,
) => { ) => {
const preloadImages = async (imageUrls) => { const preloadImages = async (imageUrls) => {
return Promise.all( return Promise.all(
@ -20,6 +21,7 @@ const usePreloadImages = (
}; };
useEffect(() => { useEffect(() => {
if (loading) return;
const imageArray: string[] = []; const imageArray: string[] = [];
Object.keys(profilePictures).flatMap((folder) => Object.keys(profilePictures).flatMap((folder) =>
@ -33,7 +35,7 @@ const usePreloadImages = (
preloadImages(imageArray).then(() => { preloadImages(imageArray).then(() => {
setImagesLoaded(true); setImagesLoaded(true);
}); });
}, [profilePictures]); }, [profilePictures, loading]);
return; return;
}; };

View file

@ -7,7 +7,7 @@ import { cn } from "../../../../../../../../utils/utils";
import usePreloadImages from "./hooks/use-preload-images"; import usePreloadImages from "./hooks/use-preload-images";
type ProfilePictureChooserComponentProps = { type ProfilePictureChooserComponentProps = {
profilePictures: { [key: string]: string[] }; profilePictures: { [key: string]: string[] } | undefined;
loading: boolean; loading: boolean;
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;
@ -29,21 +29,21 @@ export default function ProfilePictureChooserComponent({
} }
}, [ref, value]); }, [ref, value]);
usePreloadImages(profilePictures, setImagesLoaded); usePreloadImages(profilePictures!, setImagesLoaded, loading);
return ( return (
<div className="flex flex-col justify-center gap-2"> <div className="flex flex-col justify-center gap-2">
{loading || !imagesLoaded ? ( {loading || !imagesLoaded ? (
<Loading /> <Loading />
) : ( ) : (
Object.keys(profilePictures).map((folder, idx) => ( Object.keys(profilePictures!).map((folder, idx) => (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="edit-flow-arrangement"> <div className="edit-flow-arrangement">
<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-muted 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}
unstyled unstyled

View file

@ -1,7 +1,4 @@
import { import { useGetProfilePicturesQuery } from "@/controllers/API/queries/files";
ProfilePicturesResponse,
useGetProfilePicturesQuery,
} from "@/controllers/API/queries/files";
import * as Form from "@radix-ui/react-form"; import * as Form from "@radix-ui/react-form";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Button } from "../../../../../../components/ui/button"; import { Button } from "../../../../../../components/ui/button";
@ -20,7 +17,7 @@ type ProfilePictureFormComponentProps = {
profilePicture: string; profilePicture: string;
handleInput: (event: any) => void; handleInput: (event: any) => void;
handlePatchProfilePicture: (gradient: string) => void; handlePatchProfilePicture: (gradient: string) => void;
handleGetProfilePictures: () => ProfilePicturesResponse | undefined; handleGetProfilePictures: () => undefined;
userData: any; userData: any;
}; };
const ProfilePictureFormComponent = ({ const ProfilePictureFormComponent = ({
@ -30,28 +27,8 @@ const ProfilePictureFormComponent = ({
handleGetProfilePictures, handleGetProfilePictures,
userData, userData,
}: ProfilePictureFormComponentProps) => { }: ProfilePictureFormComponentProps) => {
const [profilePictures, setProfilePictures] = useState<{
[key: string]: string[];
}>({});
const { data: response, isFetching } = useGetProfilePicturesQuery({}); const { data: response, isFetching } = useGetProfilePicturesQuery({});
useEffect(() => {
if (response?.files) {
response?.files?.forEach((profile_picture) => {
const [folder, path] = profile_picture.split("/");
setProfilePictures((prev) => {
if (prev[folder]) {
prev[folder].push(path);
} else {
prev[folder] = [path];
}
return prev;
});
});
}
}, [response]);
return ( return (
<Form.Root <Form.Root
onSubmit={(event) => { onSubmit={(event) => {
@ -69,7 +46,7 @@ const ProfilePictureFormComponent = ({
<CardContent> <CardContent>
<div className="py-2"> <div className="py-2">
<ProfilePictureChooserComponent <ProfilePictureChooserComponent
profilePictures={profilePictures} profilePictures={response}
loading={isFetching} loading={isFetching}
value={ value={
profilePicture == "" profilePicture == ""