diff --git a/src/pages/AddEvent.tsx b/src/pages/AddEvent.tsx index 8392d69..b4af12b 100644 --- a/src/pages/AddEvent.tsx +++ b/src/pages/AddEvent.tsx @@ -6,6 +6,7 @@ import { useMatchEvents } from '../hooks/useMatchEvents' import { formatMatchLabel } from '../utils/matchCountdown' import { getCafeUuid } from '../utils/cafe' import Icon from '../components/Icon' +import HostBanner from '../assets/images/banners/Host-banner.jpg' const ENTRY_FEE_STEP = 10000 const DESCRIPTION_MAX_LENGTH = 200 @@ -190,7 +191,7 @@ export default function AddEvent() { return (
-
+ + + {open && + createPortal( + <> +
setOpen(false)} /> +
+ {options.map((city) => ( + + ))} +
+ , + document.body, + )} +
+ ); +} + function LocationPicker({ onSelect, }: { @@ -114,24 +186,241 @@ function CafeLocationMap({ ); } -function UploadBox({ - label, - buttonLabel, +const MAX_PHOTOS = 10; + +function ImgWithHeicFallback({ + src, + className, + fallbackClassName, }: { - label: string; - buttonLabel: string; + src: string; + className: string; + fallbackClassName: string; }) { + const [error, setError] = useState(false); + if (error) { + return ( +
+ + + + + + HEIC +
+ ); + } + return setError(true)} className={className} />; +} + +interface UploadedPhoto { + id: string; + file?: File; + localUrl: string; + status: "uploading" | "done" | "error"; + objectKey?: string; +} + +interface UploadedDocument { + file?: File; + localUrl: string; + status: "uploading" | "done" | "error"; + objectKey?: string; +} + +function PhotoUpload({ + photos, + onAdd, + onRemove, +}: { + photos: UploadedPhoto[]; + onAdd: (files: File[]) => void; + onRemove: (id: string) => void; +}) { + const inputRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); + const remaining = MAX_PHOTOS - photos.length; + const canAdd = remaining > 0; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + const files = Array.from(e.dataTransfer.files) + .filter((f) => f.type.startsWith("image/")) + .slice(0, remaining); + if (files.length > 0) onAdd(files); + }; + return (
- + + {photos.length > 0 && ( +
+ {photos.map((photo) => ( +
+ + {photo.status === "uploading" && ( +
+ آپلود... +
+ )} + {photo.status === "error" && ( +
+ خطا +
+ )} + +
+ ))} +
+ )} + + {canAdd && ( + + )} + + { + if (!e.target.files) return; + const files = Array.from(e.target.files).slice(0, remaining); + if (files.length > 0) onAdd(files); + e.target.value = ""; + }} + /> +
+ ); +} + +function DocumentUpload({ + doc, + onAdd, + onRemove, +}: { + doc: UploadedDocument | null; + onAdd: (file: File) => void; + onRemove: () => void; +}) { + const inputRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + const file = Array.from(e.dataTransfer.files).find((f) => + f.type.startsWith("image/"), + ); + if (file) onAdd(file); + }; + + return ( +
+ + + {doc ? ( +
+ + {doc.status === "uploading" && ( +
+ آپلود... +
+ )} + {doc.status === "error" && ( +
+ خطا در آپلود +
+ )} + +
+ ) : ( + + )} + + { + const file = e.target.files?.[0]; + if (file) onAdd(file); + e.target.value = ""; + }} + />
); } @@ -181,9 +470,30 @@ export default function CafeOwnerRegister() { // explicitly selected in edit mode (pre-filled from existing cafe); empty = auto-select Tehran below const [selectedCityUuid, setSelectedCityUuid] = useState(""); const [cafeFeatures, setCafeFeatures] = useState([]); + const [photos, setPhotos] = useState([]); + const [licenseDoc, setLicenseDoc] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(""); + // keep refs so the unmount cleanup can revoke any remaining object URLs + const photosRef = useRef(photos); + useEffect(() => { + photosRef.current = photos; + }, [photos]); + const licenseDocRef = useRef(licenseDoc); + useEffect(() => { + licenseDocRef.current = licenseDoc; + }, [licenseDoc]); + useEffect(() => { + return () => { + photosRef.current + .filter((p) => p.localUrl.startsWith("blob:")) + .forEach((p) => URL.revokeObjectURL(p.localUrl)); + const doc = licenseDocRef.current; + if (doc?.localUrl.startsWith("blob:")) URL.revokeObjectURL(doc.localUrl); + }; + }, []); + // redirect owners who already have a cafe away from fresh registration useEffect(() => { if (!isEditMode && getCafeUuid()) { @@ -216,12 +526,33 @@ export default function CafeOwnerRegister() { setCafeName(cafe.name); setSelectedCityUuid(cafe.city.uuid); setAddress(cafe.address); - setCafePhone(fromE164(cafe.phone_number)); + setCafePhone(cafe.phone_number); setCafeLocation({ lat: cafe.location.coordinates[1], lng: cafe.location.coordinates[0], }); setSelectedFeatures(cafe.features.map((f) => f.uuid)); + + const imageMedia = cafe.media.filter((m) => m.type === "image"); + const docMedia = cafe.media.find((m) => m.type === "document"); + + if (imageMedia.length > 0) { + setPhotos( + imageMedia.map((m) => ({ + id: m.uuid, + localUrl: m.url, + status: "done", + objectKey: m.url, + })), + ); + } + if (docMedia) { + setLicenseDoc({ + localUrl: docMedia.url, + status: "done", + objectKey: docMedia.url, + }); + } }) .catch(() => {}); }, [isEditMode]); @@ -232,14 +563,115 @@ export default function CafeOwnerRegister() { ); }; + const handleAddPhotos = async (files: File[]) => { + const newPhotos: UploadedPhoto[] = files.map((file) => ({ + id: crypto.randomUUID(), + file, + localUrl: URL.createObjectURL(file), + status: "uploading", + })); + setPhotos((prev) => [...prev, ...newPhotos]); + + await Promise.all( + newPhotos.map(async (photo) => { + try { + const { upload_url, object_key } = await presignMedia(photo.file!.name); + const res = await fetch(upload_url, { + method: "PUT", + body: photo.file!, + headers: { "Content-Type": photo.file!.type }, + }); + if (!res.ok) throw new Error("upload failed"); + setPhotos((prev) => + prev.map((p) => + p.id === photo.id ? { ...p, status: "done", objectKey: object_key } : p, + ), + ); + } catch { + setPhotos((prev) => + prev.map((p) => + p.id === photo.id ? { ...p, status: "error" } : p, + ), + ); + } + }), + ); + }; + + const handleRemovePhoto = (id: string) => { + setPhotos((prev) => { + const photo = prev.find((p) => p.id === id); + if (photo) URL.revokeObjectURL(photo.localUrl); + return prev.filter((p) => p.id !== id); + }); + }; + + const handleAddLicense = async (file: File) => { + const newDoc: UploadedDocument = { + file, + localUrl: URL.createObjectURL(file), + status: "uploading", + }; + setLicenseDoc(newDoc); + try { + const { upload_url, object_key } = await presignMedia(file.name); + const res = await fetch(upload_url, { + method: "PUT", + body: file, + headers: { "Content-Type": file.type }, + }); + if (!res.ok) throw new Error("upload failed"); + setLicenseDoc((prev) => + prev ? { ...prev, status: "done", objectKey: object_key } : null, + ); + } catch { + setLicenseDoc((prev) => + prev ? { ...prev, status: "error" } : null, + ); + } + }; + + const handleRemoveLicense = () => { + setLicenseDoc((prev) => { + if (prev) URL.revokeObjectURL(prev.localUrl); + return null; + }); + }; + const handleContinue = async () => { + const stillUploading = + photos.some((p) => p.status === "uploading") || + licenseDoc?.status === "uploading"; + if (stillUploading) { + setError("لطفاً منتظر بمانید تا آپلود فایل‌ها کامل شود."); + return; + } + if (!isEditMode) { + if (!photos.some((p) => p.status === "done")) { + setError("لطفاً حداقل یک تصویر برای کافه آپلود کنید."); + return; + } + if (!licenseDoc || licenseDoc.status !== "done") { + setError("لطفاً تصویر مجوز کسب و کار را آپلود کنید."); + return; + } + } setError(""); setIsSubmitting(true); try { + const mediaItems: CafeMediaItem[] = [ + ...photos + .filter((p) => p.status === "done" && p.objectKey) + .map((p) => ({ object_key: p.objectKey!, type: "image" as const })), + ...(licenseDoc?.status === "done" && licenseDoc.objectKey + ? [{ object_key: licenseDoc.objectKey, type: "document" as const }] + : []), + ]; + const payload = { city_uuid: effectiveCityUuid, name: cafeName, - phone_number: cafePhone ? toE164(cafePhone) : "", + phone_number: cafePhone, location: { type: "Point" as const, coordinates: [cafeLocation.lng, cafeLocation.lat] as [ @@ -249,6 +681,7 @@ export default function CafeOwnerRegister() { }, address, feature_uuids: selectedFeatures, + media: mediaItems, }; if (isEditMode) { const cafeUuid = getCafeUuid() ?? ""; @@ -272,7 +705,7 @@ export default function CafeOwnerRegister() { return (
-
+
+ - - + +