351 lines
10 KiB
TypeScript
351 lines
10 KiB
TypeScript
import { useEffect, useMemo, useRef, useState } from "react";
|
||
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,
|
||
City,
|
||
fetchCafeFeatures,
|
||
fetchCities,
|
||
fetchMyCafes,
|
||
registerCafe,
|
||
updateCafe,
|
||
} from "../services/campaignApi";
|
||
import { getCafeUuid, setCafeUuid } from "../utils/cafe";
|
||
import { fromE164, toE164 } from "../utils/phone";
|
||
|
||
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 (
|
||
<div className="mt-5">
|
||
<label className="mb-3 block text-right text-lg font-normal text-gray-900">
|
||
{label}
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={value}
|
||
onChange={(e) => 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 && (
|
||
<p className="mt-1 text-right text-sm text-gray-500">{hint}</p>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
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 (
|
||
<div className="mt-5">
|
||
<label className="mb-3 block text-right text-lg font-normal text-gray-900">
|
||
{label}
|
||
</label>
|
||
<MapContainer
|
||
center={[location.lat, location.lng]}
|
||
zoom={15}
|
||
scrollWheelZoom={false}
|
||
zoomControl={false}
|
||
className="h-48 w-full overflow-hidden rounded-2xl"
|
||
>
|
||
<TileLayer
|
||
attribution="© Addwin Maps"
|
||
url="https://maps.addwin.ir/styles/basic/{z}/{x}/{y}.png"
|
||
maxZoom={19}
|
||
minZoom={8}
|
||
/>
|
||
<Marker position={[location.lat, location.lng]} />
|
||
<ZoomControl position="topright" />
|
||
<LocationPicker onSelect={onChange} />
|
||
</MapContainer>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function UploadBox({
|
||
label,
|
||
buttonLabel,
|
||
}: {
|
||
label: string;
|
||
buttonLabel: string;
|
||
}) {
|
||
return (
|
||
<div className="mt-5">
|
||
<label className="mb-3 block text-right text-lg font-normal text-gray-900">
|
||
{label}
|
||
</label>
|
||
<button
|
||
type="button"
|
||
className="flex h-36 w-full items-center justify-center rounded-2xl border border-dashed border-gray-300 bg-[#F8F8F8] text-[#666666]"
|
||
>
|
||
{buttonLabel}
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function FeatureChip({
|
||
label,
|
||
checked,
|
||
onToggle,
|
||
}: {
|
||
label: string;
|
||
checked: boolean;
|
||
onToggle: () => void;
|
||
}) {
|
||
return (
|
||
<button
|
||
type="button"
|
||
onClick={onToggle}
|
||
className={`flex items-center gap-2 whitespace-nowrap rounded-2xl border border-[#F2F2F2] px-3 py-2 text-sm text-gray-900 ${
|
||
checked ? "bg-[#ECF2F8]" : "bg-transparent"
|
||
}`}
|
||
>
|
||
<span
|
||
className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-md border border-[#223D5D] ${
|
||
checked ? "bg-[#223D5D]" : "bg-transparent"
|
||
}`}
|
||
>
|
||
{checked && <Icon name="check" className="h-3 w-3 text-white" />}
|
||
</span>
|
||
<span>{label}</span>
|
||
</button>
|
||
);
|
||
}
|
||
|
||
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<string[]>([]);
|
||
const [cities, setCities] = useState<City[]>([]);
|
||
// explicitly selected in edit mode (pre-filled from existing cafe); empty = auto-select Tehran below
|
||
const [selectedCityUuid, setSelectedCityUuid] = useState("");
|
||
const [cafeFeatures, setCafeFeatures] = useState<CafeFeature[]>([]);
|
||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||
const [error, setError] = useState("");
|
||
|
||
// 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(fromE164(cafe.phone_number));
|
||
setCafeLocation({
|
||
lat: cafe.location.coordinates[1],
|
||
lng: cafe.location.coordinates[0],
|
||
});
|
||
setSelectedFeatures(cafe.features.map((f) => f.uuid));
|
||
})
|
||
.catch(() => {});
|
||
}, [isEditMode]);
|
||
|
||
const toggleFeature = (uuid: string) => {
|
||
setSelectedFeatures((prev) =>
|
||
prev.includes(uuid) ? prev.filter((f) => f !== uuid) : [...prev, uuid],
|
||
);
|
||
};
|
||
|
||
const handleContinue = async () => {
|
||
setError("");
|
||
setIsSubmitting(true);
|
||
try {
|
||
const payload = {
|
||
city_uuid: effectiveCityUuid,
|
||
name: cafeName,
|
||
phone_number: cafePhone ? toE164(cafePhone) : "",
|
||
location: {
|
||
type: "Point" as const,
|
||
coordinates: [cafeLocation.lng, cafeLocation.lat] as [
|
||
number,
|
||
number,
|
||
],
|
||
},
|
||
address,
|
||
feature_uuids: selectedFeatures,
|
||
};
|
||
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 (
|
||
<div className="min-h-screen bg-white pb-24">
|
||
<div className="h-16 w-full bg-gray-200" />
|
||
|
||
<div className="px-4">
|
||
<FormField
|
||
label="نام کافه"
|
||
placeholder="نام کافه را وارد کنید"
|
||
value={cafeName}
|
||
onChange={setCafeName}
|
||
/>
|
||
<FormField
|
||
label="آدرس"
|
||
placeholder="موقعیت کافه را با جزئیات وارد کنید"
|
||
value={address}
|
||
onChange={setAddress}
|
||
/>
|
||
<CafeLocationMap
|
||
label="موقعیت روی نقشه"
|
||
location={cafeLocation}
|
||
onChange={(lat, lng) => setCafeLocation({ lat, lng })}
|
||
/>
|
||
<FormField
|
||
label="شماره تماس کافه"
|
||
placeholder="مثال: ۰۲۱-۱۲۳۴۵۶۷"
|
||
hint="شماره را همراه با پیششماره شهر وارد کنید."
|
||
value={cafePhone}
|
||
onChange={setCafePhone}
|
||
/>
|
||
<UploadBox label="تصاویر" buttonLabel="افزودن تصویر" />
|
||
<UploadBox label="مجوز کسب و کار" buttonLabel="افزودن تصویر مجوز" />
|
||
|
||
<div className="mt-5">
|
||
<label className="mb-3 block text-right text-lg font-normal text-gray-900">
|
||
ویژگیهای کافه
|
||
</label>
|
||
<div className="grid grid-cols-2 gap-3">
|
||
{cafeFeatures.map((feature) => (
|
||
<FeatureChip
|
||
key={feature.uuid}
|
||
label={feature.name}
|
||
checked={selectedFeatures.includes(feature.uuid)}
|
||
onToggle={() => toggleFeature(feature.uuid)}
|
||
/>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{error && (
|
||
<p className="mt-4 text-right text-sm text-red-500">{error}</p>
|
||
)}
|
||
</div>
|
||
|
||
<div className="pointer-events-none fixed inset-x-0 bottom-0 flex items-center justify-between gap-3 bg-white px-4 py-4">
|
||
<button
|
||
type="button"
|
||
className="pointer-events-auto rounded-full bg-[#223D5D] px-5 py-3 font-semibold text-white"
|
||
>
|
||
پشتیبانی
|
||
</button>
|
||
<button
|
||
type="button"
|
||
disabled={isSubmitting}
|
||
onClick={handleContinue}
|
||
className="pointer-events-auto flex items-center gap-2 rounded-full bg-[#223D5D] px-6 py-3 font-semibold text-white disabled:opacity-50"
|
||
>
|
||
{isSubmitting
|
||
? "در حال ارسال..."
|
||
: isEditMode
|
||
? "ذخیره تغییرات"
|
||
: "ادامه"}
|
||
{!isSubmitting && !isEditMode && (
|
||
<Icon name="chevronLeft" className="h-4 w-4" />
|
||
)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|