"use client"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import AiPromptPanel from "@/components/AiPromptPanel"; import CategoryTreeSelect from "@/components/CategoryTreeSelect"; import ImageCropDialog from "@/components/ImageCropDialog"; import RichTextArea from "@/components/RichTextArea"; import { getMarketplaceBusiness, updateMarketplaceBusiness } from "@/services/marketplace"; import { buildBusinessAboutAiPrompt, buildBusinessVisionAiPrompt, fillBusinessProfileFieldByAi, type BusinessProfileAiField, } from "@/services/meshkee-business-ai"; import { getBusinessProfile, updateBusinessProfile, uploadBusinessMedia, type BusinessAddress, type BusinessPhoneNumber, } from "@/services/meshkee-businesses"; import { cityLabel, listCitiesInProvince, listIranProvinces, matchCityName, type MeshkeeCity, } from "@/services/meshkee-cities"; import { categoryLabel, searchMeshkeeCategories, type MeshkeeCategory, } from "@/services/meshkee-categories"; import styles from "./EditBusinessPage.module.css"; import richStyles from "./RichTextArea.module.css"; import aiStyles from "@/styles/ai.module.css"; const BANNER_ASPECT = 18 / 6; const LOGO_ASPECTS = [ { id: "1:1", label: "1:1", value: 1 }, { id: "3:2", label: "3:2", value: 3 / 2 }, { id: "16:9", label: "16:9", value: 16 / 9 }, ] as const; type LogoAspectId = (typeof LOGO_ASPECTS)[number]["id"]; function closestLogoAspectId(width: number, height: number): LogoAspectId { if (!width || !height) return "1:1"; const ratio = width / height; let best: LogoAspectId = LOGO_ASPECTS[0].id; let bestDist = Number.POSITIVE_INFINITY; for (const option of LOGO_ASPECTS) { const dist = Math.abs(Math.log(ratio / option.value)); if (dist < bestDist) { bestDist = dist; best = option.id; } } return best; } function detectLogoAspectId(src: string): Promise { return new Promise((resolve) => { const image = new Image(); image.onload = () => { resolve(closestLogoAspectId(image.naturalWidth, image.naturalHeight)); }; image.onerror = () => resolve("1:1"); image.src = src; }); } function slugify(value: string) { return value .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 100); } function emptyAddress(): BusinessAddress { return { name: "", province: "", city: "", address: "", postalCode: "", landline: "", }; } function emptyPhone(): BusinessPhoneNumber { return { type: "cell", number: "" }; } function toPlainText(value: string) { if (!value.includes("<")) return value; return value .replace(//gi, "\n") .replace(/<\/p>/gi, "\n") .replace(/<[^>]+>/g, "") .replace(/ /g, " ") .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/\n{3,}/g, "\n\n") .trim(); } function uniqueTags(categories: MeshkeeCategory[], ids: string[]) { const selected = new Set(ids); const tags = new Set(); for (const category of categories) { if (!selected.has(category.id)) continue; for (const tag of category.tags) { if (tag.trim()) tags.add(tag.trim()); } } return [...tags]; } export default function EditBusinessPage({ listingId, meshkeeBusinessId, variant = "admin", }: { listingId?: string; meshkeeBusinessId?: string; variant?: "admin" | "business"; }) { const router = useRouter(); const logoRef = useRef(null); const bannerRef = useRef(null); const isBusiness = variant === "business"; const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); const [error, setError] = useState(""); const [meshkeeId, setMeshkeeId] = useState(""); const [slug, setSlug] = useState(""); const [nameEn, setNameEn] = useState(""); const [nameFa, setNameFa] = useState(""); const [title, setTitle] = useState(""); const [about, setAbout] = useState(""); const [vision, setVision] = useState(""); const [categoryIds, setCategoryIds] = useState([]); const [categories, setCategories] = useState([]); const [tags, setTags] = useState([]); const [tagInput, setTagInput] = useState(""); const [phones, setPhones] = useState([emptyPhone()]); const [addresses, setAddresses] = useState([emptyAddress()]); const [provinces, setProvinces] = useState([]); const [citiesByProvinceId, setCitiesByProvinceId] = useState< Record >({}); const [logoUrl, setLogoUrl] = useState(null); const [logoMediaId, setLogoMediaId] = useState(null); const [logoFile, setLogoFile] = useState(null); const [logoRemoved, setLogoRemoved] = useState(false); const [bannerUrl, setBannerUrl] = useState(null); const [bannerMediaId, setBannerMediaId] = useState(null); const [bannerFile, setBannerFile] = useState(null); const [bannerRemoved, setBannerRemoved] = useState(false); const [cropTarget, setCropTarget] = useState<"logo" | "banner" | null>(null); const [cropSource, setCropSource] = useState(null); const [cropFileName, setCropFileName] = useState("image"); const [logoAspectId, setLogoAspectId] = useState("1:1"); const logoAspect = LOGO_ASPECTS.find((option) => option.id === logoAspectId)?.value ?? 1; const [aiField, setAiField] = useState(null); const [aiBusy, setAiBusy] = useState(false); const [aiError, setAiError] = useState(""); useEffect(() => { if (!logoUrl) { setLogoAspectId("1:1"); return; } let cancelled = false; void detectLogoAspectId(logoUrl).then((id) => { if (!cancelled) setLogoAspectId(id); }); return () => { cancelled = true; }; }, [logoUrl]); function closeCropper() { setCropTarget(null); setCropFileName("image"); setCropSource((prev) => { if (prev?.startsWith("blob:")) { requestAnimationFrame(() => URL.revokeObjectURL(prev)); } return null; }); } function openCropper(target: "logo" | "banner", file: File) { if (cropSource?.startsWith("blob:")) URL.revokeObjectURL(cropSource); setCropTarget(target); setCropSource(URL.createObjectURL(file)); setCropFileName(file.name || target); } useEffect(() => { let cancelled = false; void (async () => { setLoading(true); setError(""); try { let resolvedMeshkeeId = meshkeeBusinessId?.trim() ?? ""; let listingSlug = ""; if (!resolvedMeshkeeId) { if (!listingId) { throw new Error("Missing business id."); } const listing = await getMarketplaceBusiness(listingId); if (cancelled) return; resolvedMeshkeeId = String(listing.meshkeeBusinessId); listingSlug = listing.slug; } const [profile, cats, iranProvinces] = await Promise.all([ getBusinessProfile(resolvedMeshkeeId), searchMeshkeeCategories(""), listIranProvinces(), ]); if (cancelled) return; setMeshkeeId(resolvedMeshkeeId); setSlug(listingSlug); setCategories(cats); setProvinces(iranProvinces); setNameEn(profile.profile.nameEn); setNameFa(profile.profile.nameFa); setTitle(profile.profile.title ?? ""); setAbout(toPlainText(profile.profile.about)); setVision(profile.profile.vision); setCategoryIds(profile.profile.categoryIds); setTags(uniqueTags(cats, profile.profile.categoryIds)); setPhones( profile.profile.phoneNumbers.length ? (profile.profile.phoneNumbers as BusinessPhoneNumber[]) : [emptyPhone()], ); const nextAddresses = profile.addresses.length ? profile.addresses : [emptyAddress()]; setAddresses(nextAddresses); const cityCache: Record = {}; await Promise.all( nextAddresses.map(async (address) => { const province = matchCityName(iranProvinces, address.province); if (!province || cityCache[province.id]) return; cityCache[province.id] = await listCitiesInProvince(province.id); }), ); if (cancelled) return; setCitiesByProvinceId(cityCache); setLogoUrl(profile.profile.logoUrl); setLogoMediaId(profile.profile.logoMediaId); setBannerUrl(profile.profile.bannerUrl); setBannerMediaId(profile.profile.bannerMediaId); } catch (err) { if (cancelled) return; setError( err instanceof Error ? err.message : "Could not load business.", ); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [listingId, meshkeeBusinessId]); function setCategoriesAndTags(ids: string[]) { setCategoryIds(ids); setTags(uniqueTags(categories, ids)); } const selectedCategoryNames = categories .filter((category) => categoryIds.includes(category.id)) .map((category) => categoryLabel(category)) .join(", "); const aboutAiPrompt = buildBusinessAboutAiPrompt({ nameEn, nameFa, categories: selectedCategoryNames, }); const visionAiPrompt = buildBusinessVisionAiPrompt({ nameEn, nameFa, categories: selectedCategoryNames, }); async function runAiFill(field: BusinessProfileAiField, prompt: string) { if (!meshkeeId) return; if (!nameEn.trim() && !nameFa.trim()) { setAiError("Enter a business name before using AI."); return; } setAiBusy(true); setAiError(""); try { const result = await fillBusinessProfileFieldByAi(meshkeeId, { field, prompt, nameEn: nameEn.trim() || undefined, nameFa: nameFa.trim() || undefined, categories: selectedCategoryNames || undefined, }); if (field === "about") { setAbout(result.text ?? ""); } else { setVision(result.html ?? ""); } setAiField(null); } catch (err) { setAiError(err instanceof Error ? err.message : "AI request failed."); } finally { setAiBusy(false); } } function patchAddress(index: number, patch: Partial) { setAddresses((prev) => prev.map((item, i) => (i === index ? { ...item, ...patch } : item)), ); } async function ensureCitiesForProvince(provinceId: string) { if (citiesByProvinceId[provinceId]) return citiesByProvinceId[provinceId]; const cities = await listCitiesInProvince(provinceId); setCitiesByProvinceId((prev) => ({ ...prev, [provinceId]: cities })); return cities; } async function onProvinceChange(index: number, provinceNameEn: string) { const province = matchCityName(provinces, provinceNameEn); patchAddress(index, { province: provinceNameEn, city: "" }); if (province) { await ensureCitiesForProvince(province.id); } } function addTag(raw: string) { const tag = raw.trim(); if (!tag) return; setTags((prev) => (prev.includes(tag) ? prev : [...prev, tag])); const matches = categories.filter((category) => category.tags.some((item) => item.toLowerCase() === tag.toLowerCase()), ); if (matches.length) { setCategoryIds((prev) => { const next = new Set(prev); for (const match of matches) next.add(match.id); return [...next]; }); } setTagInput(""); } function removeTag(tag: string) { setTags((prev) => prev.filter((item) => item !== tag)); } async function onSave() { if (!meshkeeId) return; setSaving(true); setError(""); try { let nextLogoId: number | null | undefined = undefined; let nextBannerId: number | null | undefined = undefined; if (logoFile) { const uploaded = await uploadBusinessMedia(meshkeeId, logoFile); nextLogoId = Number(uploaded.id); } else if (logoRemoved) { nextLogoId = null; } if (bannerFile) { const uploaded = await uploadBusinessMedia(meshkeeId, bannerFile); nextBannerId = Number(uploaded.id); } else if (bannerRemoved) { nextBannerId = null; } const cleanPhones = phones .map((phone) => ({ type: phone.type, number: phone.number.trim(), })) .filter((phone) => phone.number.length >= 3); const cleanAddresses = addresses .map((address) => ({ id: address.id, name: address.name.trim(), province: address.province.trim(), city: address.city.trim(), address: address.address.trim(), postalCode: address.postalCode.trim(), landline: address.landline?.trim() || null, })) .filter( (address) => address.province && address.city && address.address && address.postalCode, ); await updateBusinessProfile(meshkeeId, { nameEn: nameEn.trim(), nameFa: nameFa.trim(), title: title.trim(), about, vision, categoryIds: categoryIds.map(Number), phoneNumbers: cleanPhones, addresses: cleanAddresses, ...(nextLogoId !== undefined ? { logoMediaId: nextLogoId } : {}), ...(nextBannerId !== undefined ? { bannerMediaId: nextBannerId } : {}), }); if (!isBusiness) { if (!listingId) { throw new Error("Missing listing id."); } const nextSlug = slugify(slug); if (!nextSlug) { throw new Error( "Enter a valid slug (lowercase letters, numbers, hyphens).", ); } await updateMarketplaceBusiness(listingId, { slug: nextSlug, name: nameEn.trim() || nameFa.trim() || undefined, }); setSlug(nextSlug); router.push("/businesses"); router.refresh(); } else { setSaved(true); window.setTimeout(() => setSaved(false), 2500); } } catch (err) { setError(err instanceof Error ? err.message : "Could not save business."); } finally { setSaving(false); } } return (
{isBusiness ? ( <>

Profile

Update how your business appears on Meshkee.

) : ( <>

Edit business

{slug ? `/${slug}` : "Update Meshkee profile for this listing."}

)}
{!isBusiness ? ( Back ) : null}
{loading ? (
Loading profile…
) : error && !meshkeeId ? (
{error}
) : (

Media

Logo
{logoUrl ? ( ) : null}
{LOGO_ASPECTS.map((option) => ( ))}
{ const file = event.target.files?.[0]; event.target.value = ""; if (!file) return; openCropper("logo", file); }} />
Banner · 18:6
{bannerUrl ? ( ) : null}
{ const file = event.target.files?.[0]; event.target.value = ""; if (!file) return; openCropper("banner", file); }} />
{ if (cropTarget === "logo") { if (logoUrl?.startsWith("blob:")) URL.revokeObjectURL(logoUrl); setLogoFile(file); setLogoRemoved(false); setLogoUrl(previewUrl); } else { if (bannerUrl?.startsWith("blob:")) { URL.revokeObjectURL(bannerUrl); } setBannerFile(file); setBannerRemoved(false); setBannerUrl(previewUrl); } closeCropper(); }} />

Identity

{!isBusiness ? ( ) : null}
Abstract
{ if (!aiBusy) { setAiField(null); setAiError(""); } }} onRun={(prompt) => void runAiFill("about", prompt)} />