reservation-front/src/pages/CafeOwnerRegister.tsx
2026-07-11 14:46:46 +08:00

351 lines
10 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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="&copy; 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>
);
}