"use client"; import { useEffect, useId, useRef, useState } from "react"; import { ApiError, isAbortError } from "@/lib/meshkee-api"; import { createMarketplaceBusiness } from "@/services/marketplace"; import { searchMeshkeeCategories } from "@/services/meshkee-categories"; import { createMeshkeeBusiness, getMeshkeeBusiness, listLatestMeshkeeBusinesses, searchMeshkeeBusinesses, type MeshkeeBusinessDetail, type MeshkeeBusinessSearchItem, } from "@/services/meshkee-businesses"; import { PhoneLtr, withFaSpans } from "@/lib/faText"; import styles from "./AddBusinessModal.module.css"; export type AddBusinessMode = "connect" | "create"; function slugify(value: string) { return value .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 80); } function ownerParts(detail: MeshkeeBusinessDetail | null, fallback?: { ownerName?: string | null; ownerCellNumber?: string | null; }): { name: string; cell: string } { if (detail?.owner) { const name = [detail.owner.firstName, detail.owner.lastName] .filter(Boolean) .join(" ") .trim(); return { name, cell: detail.owner.cellNumber || "" }; } return { name: fallback?.ownerName?.trim() || "", cell: fallback?.ownerCellNumber?.trim() || "", }; } function BusinessPickRow({ item, onSelect, }: { item: MeshkeeBusinessSearchItem; onSelect: (item: MeshkeeBusinessSearchItem) => void; }) { return ( onSelect(item)} > {item.name} {item.nameFa && item.nameFa !== item.name ? ( <> {" "} /{" "} {item.nameFa} > ) : null} /{item.slug} {item.ownerName ? ( <> {" · "} {withFaSpans(item.ownerName)} > ) : null} ); } export default function AddBusinessModal({ open, onClose, onConnected, }: { open: boolean; onClose: () => void; onConnected?: () => void; }) { const titleId = useId(); const closeRef = useRef(null); const [mode, setMode] = useState("connect"); const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [searching, setSearching] = useState(false); const [searchError, setSearchError] = useState(""); const [latest, setLatest] = useState([]); const [latestLoading, setLatestLoading] = useState(false); const [latestError, setLatestError] = useState(""); const [selected, setSelected] = useState( null, ); const [selectedDetail, setSelectedDetail] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const [businessName, setBusinessName] = useState(""); const [ownerFirstName, setOwnerFirstName] = useState(""); const [ownerLastName, setOwnerLastName] = useState(""); const [ownerCell, setOwnerCell] = useState("+98"); const [ownerPassword, setOwnerPassword] = useState(""); const [submitting, setSubmitting] = useState(false); const [submitError, setSubmitError] = useState(""); useEffect(() => { if (!open) return; const previousOverflow = document.body.style.overflow; document.body.style.overflow = "hidden"; closeRef.current?.focus(); const onKey = (event: KeyboardEvent) => { if (event.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); return () => { document.body.style.overflow = previousOverflow; window.removeEventListener("keydown", onKey); }; }, [open, onClose]); useEffect(() => { if (!open) { setMode("connect"); setQuery(""); setResults([]); setSearchError(""); setLatest([]); setLatestError(""); setSelected(null); setSelectedDetail(null); setBusinessName(""); setOwnerFirstName(""); setOwnerLastName(""); setOwnerCell("+98"); setOwnerPassword(""); setSubmitError(""); setSubmitting(false); } }, [open]); useEffect(() => { if (!open || mode !== "connect") return; const controller = new AbortController(); setLatestLoading(true); setLatestError(""); void listLatestMeshkeeBusinesses(5, controller.signal) .then((data) => { if (controller.signal.aborted) return; setLatest(data.items); }) .catch((err) => { if (isAbortError(err) || controller.signal.aborted) return; setLatest([]); setLatestError( err instanceof ApiError ? err.message : "Unable to load latest businesses.", ); }) .finally(() => { if (!controller.signal.aborted) setLatestLoading(false); }); return () => controller.abort(); }, [open, mode]); useEffect(() => { if (!open || mode !== "connect") return; const trimmed = query.trim(); if (trimmed.length < 2 || selected) { setResults([]); setSearching(false); setSearchError(""); return; } const controller = new AbortController(); const timer = window.setTimeout(async () => { setSearching(true); setSearchError(""); try { const data = await searchMeshkeeBusinesses(trimmed, controller.signal); if (controller.signal.aborted) return; setResults(data.items); } catch (err) { if (isAbortError(err) || controller.signal.aborted) return; setResults([]); setSearchError( err instanceof ApiError ? err.message : "Unable to search Meshkee businesses.", ); } finally { if (!controller.signal.aborted) setSearching(false); } }, 250); return () => { controller.abort(); window.clearTimeout(timer); }; }, [open, mode, query, selected]); async function selectBusiness(item: MeshkeeBusinessSearchItem) { setSelected(item); setQuery(item.label || item.name); setResults([]); setSubmitError(""); setDetailLoading(true); setSelectedDetail(null); try { const detail = await getMeshkeeBusiness(String(item.id)); setSelectedDetail(detail); } catch (err) { setSelectedDetail(null); setSubmitError( err instanceof Error ? err.message : "Could not load business details.", ); } finally { setDetailLoading(false); } } const showSearchResults = !selected && query.trim().length >= 2; const showLatestShortcut = !selected && query.trim().length < 2; if (!open) return null; return ( event.stopPropagation()} > Marketplace Add business × setMode("connect")} > Connect Meshkee setMode("create")} > Create new {mode === "connect" ? "Choose an existing Meshkee business to list on the marketplace." : "Create a Meshkee business with an owner, then list it here."} {mode === "connect" ? ( { event.preventDefault(); if (!selected) { setSubmitError("Select a Meshkee business from search first."); return; } setSubmitting(true); setSubmitError(""); try { await createMarketplaceBusiness({ meshkeeBusinessId: String(selected.id), slug: selected.slug, name: selected.name, status: "draft", }); onConnected?.(); onClose(); } catch (err) { setSubmitError( err instanceof Error ? err.message : "Could not connect business.", ); } finally { setSubmitting(false); } }} > Search Meshkee { setSelected(null); setSelectedDetail(null); setQuery(event.target.value); setSubmitError(""); }} placeholder="Type at least 2 characters…" autoComplete="off" disabled={submitting} /> {selected ? ( { setSelected(null); setSelectedDetail(null); setQuery(""); }} > Clear selection ) : null} {showSearchResults ? ( {searching ? ( Searching… ) : searchError ? ( {searchError} ) : results.length === 0 ? ( No matches ) : ( results.map((item) => ( void selectBusiness(picked)} /> )) )} ) : null} {showLatestShortcut ? ( Latest on Meshkee {latestLoading ? ( Loading… ) : latestError ? ( {latestError} ) : latest.length === 0 ? ( No businesses yet ) : ( latest.map((item) => ( void selectBusiness(picked)} /> )) )} ) : null} {selected ? ( Business {selected.name} {selected.nameFa && selected.nameFa !== selected.name ? ( <> {" / "} {selected.nameFa} > ) : null} Owner {detailLoading ? ( "Loading…" ) : ( (() => { const { name, cell } = ownerParts( selectedDetail, selected, ); if (!name && !cell) return "No owner linked"; return ( <> {name ? withFaSpans(name) : null} {name && cell ? " · " : null} {cell ? : null} > ); })() )} ) : null} {submitError ? ( {submitError} ) : null} Cancel {submitting ? "…" : "Connect"} ) : ( { event.preventDefault(); setSubmitting(true); setSubmitError(""); try { const categories = await searchMeshkeeCategories("other"); const other = categories.find((c) => c.slug === "other") ?? categories[0]; if (!other) { throw new Error("Meshkee “Other” category not found."); } const name = businessName.trim(); const slug = slugify(name); if (!slug) throw new Error("Enter a valid business name."); const created = await createMeshkeeBusiness({ name, nameFa: name, slug, categoryIds: [Number(other.id)], ownerFirstName: ownerFirstName.trim(), ownerLastName: ownerLastName.trim(), ownerFirstNameEn: ownerFirstName.trim(), ownerLastNameEn: ownerLastName.trim(), ownerCellNumber: ownerCell.trim(), ownerPassword, }); await createMarketplaceBusiness({ meshkeeBusinessId: created.id, slug: created.slug || slug, name: created.name || name, status: "draft", }); onConnected?.(); onClose(); } catch (err) { setSubmitError( err instanceof Error ? err.message : "Could not create business.", ); } finally { setSubmitting(false); } }} > Business name setBusinessName(event.target.value)} placeholder="Business name" required autoComplete="organization" disabled={submitting} /> Owner first name setOwnerFirstName(event.target.value)} required autoComplete="given-name" disabled={submitting} /> Owner last name setOwnerLastName(event.target.value)} required autoComplete="family-name" disabled={submitting} /> Owner cell setOwnerCell(event.target.value)} placeholder="+98912…" pattern="^\+[1-9]\d{6,14}$" required autoComplete="tel" inputMode="tel" dir="ltr" disabled={submitting} /> Owner password setOwnerPassword(event.target.value)} minLength={8} required autoComplete="new-password" disabled={submitting} /> {submitError ? ( {submitError} ) : null} Cancel {submitting ? "…" : "Create"} )} ); }
Marketplace
{mode === "connect" ? "Choose an existing Meshkee business to list on the marketplace." : "Create a Meshkee business with an owner, then list it here."}
Searching…
{searchError}
No matches
Latest on Meshkee
Loading…
{latestError}
No businesses yet
Business {selected.name} {selected.nameFa && selected.nameFa !== selected.name ? ( <> {" / "} {selected.nameFa} > ) : null}
Owner {detailLoading ? ( "Loading…" ) : ( (() => { const { name, cell } = ownerParts( selectedDetail, selected, ); if (!name && !cell) return "No owner linked"; return ( <> {name ? withFaSpans(name) : null} {name && cell ? " · " : null} {cell ? : null} > ); })() )}
{submitError}