From 35d1c34ed86d439d0fb2658da048ee2c4146be67 Mon Sep 17 00:00:00 2001 From: lemon Date: Wed, 6 May 2026 19:27:34 -0700 Subject: [PATCH] Add image uploads to event creation --- src/components/CreateCommunityEventDialog.tsx | 126 ++++++++++++++++-- 1 file changed, 116 insertions(+), 10 deletions(-) diff --git a/src/components/CreateCommunityEventDialog.tsx b/src/components/CreateCommunityEventDialog.tsx index b907eba4..a290730d 100644 --- a/src/components/CreateCommunityEventDialog.tsx +++ b/src/components/CreateCommunityEventDialog.tsx @@ -1,5 +1,5 @@ -import { useCallback, useMemo, useState } from 'react'; -import { CalendarDays, ChevronLeft } from 'lucide-react'; +import { useCallback, useMemo, useRef, useState } from 'react'; +import { CalendarDays, ChevronLeft, Loader2, Upload, X } from 'lucide-react'; import { useQueryClient } from '@tanstack/react-query'; import { @@ -16,9 +16,12 @@ import { ScrollArea } from '@/components/ui/scroll-area'; import { Switch } from '@/components/ui/switch'; import { Textarea } from '@/components/ui/textarea'; import { useCurrentUser } from '@/hooks/useCurrentUser'; +import { useAppContext } from '@/hooks/useAppContext'; import { useNostrPublish } from '@/hooks/useNostrPublish'; import { usePublishRSVP } from '@/hooks/usePublishRSVP'; import { useToast } from '@/hooks/useToast'; +import { useUploadFile } from '@/hooks/useUploadFile'; +import { resizeImage } from '@/lib/resizeImage'; import { sanitizeUrl } from '@/lib/sanitizeUrl'; interface CreateCommunityEventDialogProps { @@ -53,10 +56,13 @@ function parseCommunityAuthor(communityATag: string): string | undefined { export function CreateCommunityEventDialog({ communityATag, open, onOpenChange }: CreateCommunityEventDialogProps) { const { user } = useCurrentUser(); + const { config } = useAppContext(); const { toast } = useToast(); const queryClient = useQueryClient(); const { mutateAsync: publishEvent, isPending } = useNostrPublish(); const { mutateAsync: publishRSVP } = usePublishRSVP(); + const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile(); + const imageInputRef = useRef(null); const [step, setStep] = useState<1 | 2>(1); const [title, setTitle] = useState(''); @@ -102,13 +108,60 @@ export function CreateCommunityEventDialog({ communityATag, open, onOpenChange } }, [title, toast]); const handleNext = useCallback(() => { + if (isUploading) return; if (!validateInfoStep()) return; setStep(2); - }, [validateInfoStep]); + }, [isUploading, validateInfoStep]); + + const handleImageFile = useCallback(async (file: File) => { + if (!file.type.startsWith('image/')) { + toast({ title: 'Invalid file', description: 'Please choose an image file.', variant: 'destructive' }); + return; + } + + try { + const uploadableFile = config.imageQuality === 'compressed' + ? (await resizeImage(file)).file + : file; + const [[, url]] = await uploadFile(uploadableFile); + setImageUrl(url); + toast({ title: 'Image uploaded' }); + } catch (err) { + toast({ + title: 'Upload failed', + description: err instanceof Error ? err.message : 'Please try again.', + variant: 'destructive', + }); + } + }, [config.imageQuality, toast, uploadFile]); + + const handleImagePaste = useCallback((e: React.ClipboardEvent) => { + const items = e.clipboardData?.items; + if (!items) return; + + for (const item of Array.from(items)) { + if (!item.type.startsWith('image/')) continue; + const file = item.getAsFile(); + if (!file) return; + e.preventDefault(); + void handleImageFile(file); + return; + } + }, [handleImageFile]); + + const handleImageDrop = useCallback((e: React.DragEvent) => { + e.preventDefault(); + const file = e.dataTransfer.files[0]; + if (file) void handleImageFile(file); + }, [handleImageFile]); const handleSubmit = useCallback(async (e: React.FormEvent) => { e.preventDefault(); if (!user) return; + if (isUploading) { + toast({ title: 'Image is still uploading', description: 'Please wait for the upload to finish.' }); + return; + } if (!validateInfoStep()) return; if (!startDate) { @@ -242,6 +295,7 @@ export function CreateCommunityEventDialog({ communityATag, open, onOpenChange } endTime, handleOpenChange, imageUrl, + isUploading, location, publishEvent, publishRSVP, @@ -272,7 +326,7 @@ export function CreateCommunityEventDialog({ communityATag, open, onOpenChange }
- +
{step === 1 ? ( <> @@ -299,14 +353,66 @@ export function CreateCommunityEventDialog({ communityATag, open, onOpenChange }
- + +
+
imageInputRef.current?.click()} + onDrop={handleImageDrop} + onDragOver={(e) => e.preventDefault()} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') imageInputRef.current?.click(); + }} + className="relative flex min-h-28 w-full cursor-pointer flex-col items-center justify-center overflow-hidden rounded-t-xl border border-b-0 border-dashed border-border bg-secondary/20 text-center transition-colors hover:bg-secondary/35 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" + > + {isUploading ? ( +
+ + Uploading image... +
+ ) : sanitizeUrl(imageUrl) ? ( + <> + Event image preview + + + ) : ( +
+ + Paste, drop, or click to upload an image +
+ )} + { + const file = e.target.files?.[0]; + if (file) void handleImageFile(file); + }} + /> +
setImageUrl(e.target.value)} + className="rounded-t-none rounded-b-xl" /> +
) : ( @@ -393,8 +499,8 @@ export function CreateCommunityEventDialog({ communityATag, open, onOpenChange } - ) : ( @@ -403,8 +509,8 @@ export function CreateCommunityEventDialog({ communityATag, open, onOpenChange } Back - )}