So I'm aking this trello-like app, and I have my form for creating Boards in a shadcn Popover component. they can select a background image from unsplash or select their own, for that i made a FormPicker component, I wanted to use cloudinary for hosting the images but when i click the Upload your own button the event propagation makes everything close. I can see the widget for a second but then it's gone. This is my FormPicker component:
const FormPicker = ({ id, errors }: FormPickerProps) => {
const { pending } = useFormStatus();
const [images, setImages] = useState<Array<Record<string, any>>>([]);
const [isLoading, setIsLoading] = useState(true);
const [selectedImageId, setSelectedImageId] = useState<string | null>(null);
useEffect(() => {
const fetchImages = async () => {
try {
const result = await unsplash.photos.getRandom({
collectionIds: ["317099"],
count: 9,
});
if (result && result.response) {
const fetchedImages = result.response as Array<Record<string, any>>;
setImages(fetchedImages);
} else {
console.log("Failed to fetch Images");
}
} catch (error) {
console.log(error);
setImages([defaultImages]);
} finally {
setIsLoading(false);
}
};
fetchImages();
}, []);
const handleUploadSuccess = (result: any) => {
console.log(result.info.secure_url);
// Do something with the uploaded image URL
};
if (isLoading) {
return (
<div className="p-6 flex items-center justify-center">
<Loader2 className="w-6 h-6 text-sky-700 animate-spin" />
</div>
);
}
return (
<div className="relative">
<p className="text-neutral-700 font-semibold text-xs mb-2">
Select a background for your board
</p>
<div className="grid grid-cols-3 gap-2 mb-2">
{images.map((img) => (
<div
key={img.id}
className={cn(
"aspect-video relative transition group hover:opacity-75 bg-muted shadow-sm cursor-pointer",
pending && "opacity-50 hover:opacity-100 cursor-auto"
)}
onClick={() => {
if (pending) return;
setSelectedImageId(id);
}}
>
<Image
src={img.urls.thumb}
alt={img.alt_description}
fill
className="object-cover rounded-sm"
/>
</div>
))}
</div>
<div className="">
<div className="relative z-10">
<CldUploadWidget uploadPreset={process.env.CLOUDINARY_PRESET} onSuccess={handleUploadSuccess}>
{({ open }) => {
return (
<Button
size="sm"
type="button"
disabled={pending}
className="text-xs text-right font-semibold text-neutral-600"
variant="ghost"
onClick={() => {open()}}
>
Upload your own
</Button>
);
}}
</CldUploadWidget>
</div>
</div>
</div>
);
};
this is my FormPopover:
const FormPopover = ({
children,
side = "bottom",
align,
sideOffset = 0,
}: IFormPopoverProps) => {
const { execute, fieldErrors } = useAction(createBoard, {
onSuccess: (data) => {
console.log({ data });
toast.success("Board Created");
},
onError: (error) => {
console.error(error);
toast.error(error);
// TODO: Cambiar color del icono de toast
},
});
const onSubmit = (formData: FormData) => {
const title = formData.get("title") as string;
execute({ title });
};
return (
<div>
<Popover>
<PopoverTrigger asChild>{children}</PopoverTrigger>
<PopoverContent
align={align}
className="w-80 pt-3 "
side={side}
sideOffset={sideOffset}
>
<div className="text-sm font-medium text-center text-neutral-600 pb-4 ">
Create Board
</div>
<PopoverClose asChild>
<Button
className="h-auto w-auto absolute text-neutral-600 right-2 top-2"
variant="ghost"
>
<X className="h-4 w-4" />
</Button>
</PopoverClose>
<form action={onSubmit} className="space-y-4">
<div className="space-y-4">
<FormPicker id="image" errors={fieldErrors} />
<FormInput
id="title"
label="Board Title"
type="text"
errors={fieldErrors}
/>
</div>
<FormSubmit className="w-full" variant="primary">
Create
</FormSubmit>
</form>
</PopoverContent>
</Popover>
</div>
);
};
export default FormPopover;
I tried using state to stop the event propagation but that stops the widget from showing up. I keep trying alternatives like that but still no luck
const FormPicker = ({ id, errors }: FormPickerProps) => {
const { pending } = useFormStatus();
const [images, setImages] = useState<Array<Record<string, any>>>([]);
const [isLoading, setIsLoading] = useState(true);
const [selectedImageId, setSelectedImageId] = useState<string | null>(null);
const [isWidgetOpen, setIsWidgetOpen] = useState(false);
useEffect(() => {
const fetchImages = async () => {
try {
const result = await unsplash.photos.getRandom({
collectionIds: ["317099"],
count: 9,
});
if (result && result.response) {
const fetchedImages = result.response as Array<Record<string, any>>;
setImages(fetchedImages);
} else {
console.log("Failed to fetch Images");
}
} catch (error) {
console.log(error);
setImages([defaultImages]);
} finally {
setIsLoading(false);
}
};
fetchImages();
}, []);
const handleUploadSuccess = (result: any) => {
console.log(result.info.secure_url);
// Do something with the uploaded image URL
setIsWidgetOpen(false);
};
const openUploadWidget = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
setIsWidgetOpen(true);
};
const handleWidgetClose = () => {
setIsWidgetOpen(false);
};
if (isLoading) {
return (
<div className="p-6 flex items-center justify-center">
<Loader2 className="w-6 h-6 text-sky-700 animate-spin" />
</div>
);
}
return (
<div className="relative">
<p className="text-neutral-700 font-semibold text-xs mb-2">
Select a background for your board
</p>
<div className="grid grid-cols-3 gap-2 mb-2">
{images.map((img) => (
<div
key={img.id}
className={cn(
"aspect-video relative transition group hover:opacity-75 bg-muted shadow-sm cursor-pointer",
pending && "opacity-50 hover:opacity-100 cursor-auto"
)}
onClick={() => {
if (pending) return;
setSelectedImageId(id);
}}
>
<Image
src={img.urls.thumb}
alt={img.alt_description}
fill
className="object-cover rounded-sm"
/>
</div>
))}
</div>
<div className="relative z-10">
{isWidgetOpen && (
<CldUploadWidget
uploadPreset={process.env.CLOUDINARY_PRESET}
onSuccess={handleUploadSuccess}
onClose={handleWidgetClose}
options={{ sources: ['local', 'url'] }}
/>
)}
<Button
size="sm"
type="button"
disabled={pending}
className="text-xs text-right font-semibold text-neutral-600"
variant="ghost"
onClick={openUploadWidget}
>
Upload your own
</Button>
</div>
</div>
);
};
Note: I already checked and the widget does render properly if it's not inside the popover. My app never breaks and the console doesn't log any error