Add image uploads to event creation
This commit is contained in:
@@ -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<HTMLInputElement>(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 }
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<ScrollArea className="max-h-[62vh]">
|
||||
<ScrollArea className="max-h-[62vh]" onPaste={handleImagePaste}>
|
||||
<div className="px-5 pb-5 space-y-4">
|
||||
{step === 1 ? (
|
||||
<>
|
||||
@@ -299,14 +353,66 @@ export function CreateCommunityEventDialog({ communityATag, open, onOpenChange }
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="community-event-image">Image URL (optional)</Label>
|
||||
<Label htmlFor="community-event-image">Image (recommended)</Label>
|
||||
<div>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => 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 ? (
|
||||
<div className="flex flex-col items-center gap-2 text-muted-foreground">
|
||||
<Loader2 className="size-5 animate-spin" />
|
||||
<span className="text-xs">Uploading image...</span>
|
||||
</div>
|
||||
) : sanitizeUrl(imageUrl) ? (
|
||||
<>
|
||||
<img src={sanitizeUrl(imageUrl)} alt="Event image preview" className="absolute inset-0 h-full w-full object-cover" />
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Remove image"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setImageUrl('');
|
||||
if (imageInputRef.current) imageInputRef.current.value = '';
|
||||
}}
|
||||
className="absolute right-2 top-2 rounded-full bg-background/90 p-1 text-muted-foreground shadow-sm transition-colors hover:text-destructive"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-2 px-4 text-muted-foreground">
|
||||
<Upload className="size-5 opacity-50" />
|
||||
<span className="text-xs">Paste, drop, or click to upload an image</span>
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
ref={imageInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) void handleImageFile(file);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
id="community-event-image"
|
||||
type="url"
|
||||
placeholder="https://..."
|
||||
placeholder="Paste an image URL, or upload above"
|
||||
value={imageUrl}
|
||||
onChange={(e) => setImageUrl(e.target.value)}
|
||||
className="rounded-t-none rounded-b-xl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
@@ -393,8 +499,8 @@ export function CreateCommunityEventDialog({ communityATag, open, onOpenChange }
|
||||
<Button type="button" variant="outline" className="flex-1" onClick={() => handleOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="button" className="flex-1" onClick={handleNext}>
|
||||
Next
|
||||
<Button type="button" className="flex-1" onClick={handleNext} disabled={isUploading}>
|
||||
{isUploading ? 'Uploading...' : 'Next'}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
@@ -403,8 +509,8 @@ export function CreateCommunityEventDialog({ communityATag, open, onOpenChange }
|
||||
<ChevronLeft className="size-4" />
|
||||
Back
|
||||
</Button>
|
||||
<Button type="submit" className="flex-1" disabled={isPending}>
|
||||
{isPending ? 'Creating...' : 'Create Event'}
|
||||
<Button type="submit" className="flex-1" disabled={isPending || isUploading}>
|
||||
{isPending ? 'Creating...' : isUploading ? 'Uploading...' : 'Create Event'}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user