import { useEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { useLocation, useNavigate } from "react-router-dom"; import { MapContainer, TileLayer, Marker, ZoomControl, useMapEvents } from "react-leaflet"; import "leaflet/dist/leaflet.css"; import "../utils/leafletIcons"; import Icon from "../components/Icon"; import { CafeFeature, CafeMediaItem, City, fetchCafeFeatures, fetchCities, fetchMyCafes, presignMedia, registerCafe, updateCafe, } from "../services/campaignApi"; import { getCafeUuid, setCafeUuid } from "../utils/cafe"; import { toPersianDigits } from "../utils/formatNumber"; import HostBanner from "../assets/images/banners/Host-banner.jpg"; const DEFAULT_CAFE_LOCATION = { lat: 35.6892, lng: 51.389 }; function findTehran(cities: City[]): City | undefined { return cities.find( (c) => c.city_name === "تهران" || c.city_name.toLowerCase() === "tehran", ); } function FormField({ label, placeholder, hint, value, onChange, }: { label: string; placeholder: string; hint?: string; value: string; onChange: (value: string) => void; }) { return (
onChange(e.target.value)} placeholder={placeholder} className="w-full rounded-2xl border border-gray-200 px-4 py-3 text-right h-14 placeholder:font-light placeholder:text-[#999999]" /> {hint && (

{hint}

)}
); } function CitySelectField({ value, options, onChange, }: { value: string; options: City[]; onChange: (uuid: string) => void; }) { const [open, setOpen] = useState(false); const [dropPos, setDropPos] = useState({ top: 0, left: 0, width: 0 }); const buttonRef = useRef(null); const selected = options.find((c) => c.uuid === value); const handleOpen = () => { if (!open && buttonRef.current) { const r = buttonRef.current.getBoundingClientRect(); setDropPos({ top: r.bottom + 8, left: r.left, width: r.width }); } setOpen((o) => !o); }; return (
{open && createPortal( <>
setOpen(false)} />
{options.map((city) => ( ))}
, document.body, )}
); } function LocationPicker({ onSelect, }: { onSelect: (lat: number, lng: number) => void; }) { const onSelectRef = useRef(onSelect); useEffect(() => { onSelectRef.current = onSelect; }, [onSelect]); const handlers = useMemo( () => ({ click(e: { latlng: { lat: number; lng: number } }) { onSelectRef.current(e.latlng.lat, e.latlng.lng); }, }), [], ); useMapEvents(handlers); return null; } function CafeLocationMap({ label, location, onChange, }: { label: string; location: { lat: number; lng: number }; onChange: (lat: number, lng: number) => void; }) { return (
); } const MAX_PHOTOS = 10; function ImgWithHeicFallback({ src, className, fallbackClassName, }: { 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 = ""; }} />
); } function FeatureChip({ label, checked, onToggle, }: { label: string; checked: boolean; onToggle: () => void; }) { return ( ); } export default function CafeOwnerRegister() { const navigate = useNavigate(); const location = useLocation(); // panel's "اطلاعات کافه" link navigates here with state:{step:2} to signal edit mode const isEditMode = (location.state as { step?: number } | null)?.step === 2; const [cafeName, setCafeName] = useState(""); const [address, setAddress] = useState(""); const [cafePhone, setCafePhone] = useState(""); const [cafeLocation, setCafeLocation] = useState(DEFAULT_CAFE_LOCATION); const [selectedFeatures, setSelectedFeatures] = useState([]); const [cities, setCities] = useState([]); // 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()) { navigate("/owner/panel", { replace: true }); } }, [isEditMode, navigate]); useEffect(() => { fetchCities() .then(setCities) .catch(() => {}); fetchCafeFeatures() .then(setCafeFeatures) .catch(() => {}); }, []); // fall back to Tehran when no city has been explicitly selected yet const effectiveCityUuid = selectedCityUuid || findTehran(cities)?.uuid || cities[0]?.uuid || ""; useEffect(() => { if (!isEditMode) return; fetchMyCafes() .then((cafes) => { const cafe = cafes[0]; if (!cafe) return; setCafeName(cafe.name); setSelectedCityUuid(cafe.city.uuid); setAddress(cafe.address); 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]); const toggleFeature = (uuid: string) => { setSelectedFeatures((prev) => prev.includes(uuid) ? prev.filter((f) => f !== uuid) : [...prev, uuid], ); }; 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, location: { type: "Point" as const, coordinates: [cafeLocation.lng, cafeLocation.lat] as [ number, number, ], }, address, feature_uuids: selectedFeatures, media: mediaItems, }; if (isEditMode) { const cafeUuid = getCafeUuid() ?? ""; await updateCafe(cafeUuid, payload); navigate("/owner/panel"); } else { const cafe = await registerCafe({ owner_uuid: "", ...payload }); setCafeUuid(cafe.uuid); navigate("/owner/register/success"); } } catch { if (!isEditMode) { navigate("/owner/register/fail"); } else { setError("ذخیره تغییرات با خطا مواجه شد. لطفاً دوباره تلاش کنید."); } } finally { setIsSubmitting(false); } }; return (
setCafeLocation({ lat, lng })} />
{cafeFeatures.map((feature) => ( toggleFeature(feature.uuid)} /> ))}
{error && (

{error}

)}
); }