438 lines
15 KiB
TypeScript
438 lines
15 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import BlogPanel from "@/components/catalog/BlogPanel";
|
|
import PortfoliosPanel from "@/components/catalog/PortfoliosPanel";
|
|
import ProductsPanel from "@/components/catalog/ProductsPanel";
|
|
import { toPlainText } from "@/components/catalog/utils";
|
|
import { openBusinessDashboardAsAdmin } from "@/lib/session-handoff";
|
|
import { getMarketplaceBusiness } from "@/services/marketplace";
|
|
import {
|
|
getBusinessProfile,
|
|
getMeshkeeBusiness,
|
|
type BusinessAddress,
|
|
type BusinessPhoneNumber,
|
|
} from "@/services/meshkee-businesses";
|
|
import {
|
|
listMeshkeeBlogs,
|
|
listMeshkeePortfolios,
|
|
listMeshkeeProducts,
|
|
} from "@/services/meshkee-content";
|
|
import styles from "./BusinessDetailsPage.module.css";
|
|
|
|
type TabId = "products" | "contact" | "blog" | "portfolios";
|
|
|
|
const TAB_LABELS: Record<TabId, string> = {
|
|
products: "Products",
|
|
contact: "Contact info",
|
|
blog: "Blog",
|
|
portfolios: "Portfolios",
|
|
};
|
|
|
|
const SOCIAL_ICONS: Record<string, string> = {
|
|
instagram: "/images/icons/instagram.png",
|
|
telegram: "/images/icons/telegram.png",
|
|
facebook: "/images/icons/facebook.png",
|
|
whatsapp: "/images/icons/telegram.png",
|
|
linkedin: "/images/icons/facebook.png",
|
|
youtube: "/images/icons/facebook.png",
|
|
aparat: "/images/icons/facebook.png",
|
|
};
|
|
|
|
function hasContactData(input: {
|
|
addresses: BusinessAddress[];
|
|
phones: BusinessPhoneNumber[];
|
|
emails: string[];
|
|
socials: { network: string; handle: string }[];
|
|
}) {
|
|
return (
|
|
input.addresses.some(
|
|
(item) =>
|
|
item.address.trim() ||
|
|
item.city.trim() ||
|
|
item.province.trim() ||
|
|
(item.landline ?? "").trim(),
|
|
) ||
|
|
input.phones.some((item) => item.number.trim()) ||
|
|
input.emails.some((item) => item.trim()) ||
|
|
input.socials.length > 0
|
|
);
|
|
}
|
|
|
|
export default function BusinessDetailsPage({
|
|
listingId,
|
|
}: {
|
|
listingId: string;
|
|
}) {
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState("");
|
|
const [meshkeeId, setMeshkeeId] = useState("");
|
|
const [tab, setTab] = useState<TabId>("products");
|
|
|
|
const [name, setName] = useState("");
|
|
const [slug, setSlug] = useState("");
|
|
const [categoryName, setCategoryName] = useState("");
|
|
const [logoUrl, setLogoUrl] = useState<string | null>(null);
|
|
const [bannerUrl, setBannerUrl] = useState<string | null>(null);
|
|
const [about, setAbout] = useState("");
|
|
const [vision, setVision] = useState("");
|
|
const [sinceMonth, setSinceMonth] = useState("");
|
|
const [sinceYear, setSinceYear] = useState("");
|
|
const [province, setProvince] = useState("");
|
|
const [telephone, setTelephone] = useState("");
|
|
|
|
const [productTotal, setProductTotal] = useState(0);
|
|
const [blogTotal, setBlogTotal] = useState(0);
|
|
const [portfolioTotal, setPortfolioTotal] = useState(0);
|
|
|
|
const [addresses, setAddresses] = useState<BusinessAddress[]>([]);
|
|
const [phones, setPhones] = useState<BusinessPhoneNumber[]>([]);
|
|
const [emails, setEmails] = useState<string[]>([]);
|
|
const [socials, setSocials] = useState<
|
|
{ network: string; handle: string; icon: string }[]
|
|
>([]);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
|
|
void (async () => {
|
|
setLoading(true);
|
|
setError("");
|
|
try {
|
|
const listing = await getMarketplaceBusiness(listingId);
|
|
const id = String(listing.meshkeeBusinessId);
|
|
|
|
const [
|
|
meshkee,
|
|
profile,
|
|
productsRes,
|
|
blogsRes,
|
|
portfoliosRes,
|
|
] = await Promise.all([
|
|
getMeshkeeBusiness(id),
|
|
getBusinessProfile(id),
|
|
listMeshkeeProducts(id, { page: 1, pageSize: 1 }).catch(() => ({
|
|
total: 0,
|
|
})),
|
|
listMeshkeeBlogs(id, { page: 1, pageSize: 1 }).catch(() => ({
|
|
total: 0,
|
|
})),
|
|
listMeshkeePortfolios(id, { page: 1, pageSize: 1 }).catch(() => ({
|
|
total: 0,
|
|
})),
|
|
]);
|
|
|
|
if (cancelled) return;
|
|
|
|
const nextAddresses = profile.addresses ?? [];
|
|
const nextPhones = profile.profile.phoneNumbers ?? [];
|
|
const nextEmails = (profile.profile.emails ?? []).filter(Boolean);
|
|
const socialMedia = profile.profile.socialMedia ?? {};
|
|
const nextSocials = Object.entries(socialMedia)
|
|
.filter(([, handle]) => Boolean(handle?.trim()))
|
|
.map(([network, handle]) => ({
|
|
network,
|
|
handle: handle.trim(),
|
|
icon: SOCIAL_ICONS[network] ?? "/images/icons/telegram.png",
|
|
}));
|
|
|
|
setMeshkeeId(id);
|
|
setName(meshkee.name || listing.slug);
|
|
setSlug(listing.slug);
|
|
setCategoryName(meshkee.categories[0]?.name ?? "");
|
|
setLogoUrl(profile.profile.logoUrl ?? meshkee.logoUrl);
|
|
setBannerUrl(profile.profile.bannerUrl ?? meshkee.bannerUrl);
|
|
setAbout(profile.profile.about ?? "");
|
|
setVision(profile.profile.vision ?? "");
|
|
if (meshkee.createdAt) {
|
|
const joined = new Date(meshkee.createdAt);
|
|
setSinceMonth(
|
|
joined.toLocaleDateString("en-US", { month: "long" }),
|
|
);
|
|
setSinceYear(String(joined.getFullYear()));
|
|
} else {
|
|
setSinceMonth("");
|
|
setSinceYear("");
|
|
}
|
|
setProvince(nextAddresses[0]?.province ?? "");
|
|
setTelephone(
|
|
nextPhones.find((item) => item.number.trim())?.number ??
|
|
nextAddresses.find((item) => (item.landline ?? "").trim())
|
|
?.landline ??
|
|
"",
|
|
);
|
|
setProductTotal(productsRes.total ?? 0);
|
|
setBlogTotal(blogsRes.total ?? 0);
|
|
setPortfolioTotal(portfoliosRes.total ?? 0);
|
|
setAddresses(nextAddresses);
|
|
setPhones(nextPhones as BusinessPhoneNumber[]);
|
|
setEmails(nextEmails);
|
|
setSocials(nextSocials);
|
|
} catch (err) {
|
|
if (cancelled) return;
|
|
setError(
|
|
err instanceof Error
|
|
? err.message
|
|
: "Could not load business details.",
|
|
);
|
|
} finally {
|
|
if (!cancelled) setLoading(false);
|
|
}
|
|
})();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [listingId]);
|
|
|
|
const contactReady = hasContactData({
|
|
addresses,
|
|
phones,
|
|
emails,
|
|
socials,
|
|
});
|
|
|
|
const availableTabs = useMemo(() => {
|
|
const tabs: TabId[] = [];
|
|
if (productTotal > 0) tabs.push("products");
|
|
if (contactReady) tabs.push("contact");
|
|
if (blogTotal > 0) tabs.push("blog");
|
|
// Gallery disabled until a dedicated gallery section exists (media library ≠ gallery).
|
|
if (portfolioTotal > 0) tabs.push("portfolios");
|
|
return tabs;
|
|
}, [productTotal, contactReady, blogTotal, portfolioTotal]);
|
|
|
|
useEffect(() => {
|
|
if (!availableTabs.length) return;
|
|
if (!availableTabs.includes(tab)) setTab(availableTabs[0]);
|
|
}, [availableTabs, tab]);
|
|
|
|
const description = toPlainText(about) || toPlainText(vision);
|
|
|
|
function openBusinessDashboard(nextPath: string) {
|
|
if (!meshkeeId) return;
|
|
openBusinessDashboardAsAdmin(meshkeeId, nextPath);
|
|
}
|
|
|
|
if (loading) {
|
|
return <div className={styles.loading}>Loading business details…</div>;
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className={styles.error} role="alert">
|
|
<p className={styles.errorTitle}>Could not load details</p>
|
|
<p className={styles.errorCopy}>{error}</p>
|
|
<Link href="/businesses" className={styles.backLink}>
|
|
Back to businesses
|
|
</Link>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={styles.page}>
|
|
<header className={styles.head}>
|
|
<nav className={styles.breadcrumb} aria-label="Breadcrumb">
|
|
<Link href="/">Home</Link>
|
|
<span className={styles.breadcrumbSep} aria-hidden>
|
|
/
|
|
</span>
|
|
<Link href="/businesses">Businesses</Link>
|
|
<span className={styles.breadcrumbSep} aria-hidden>
|
|
/
|
|
</span>
|
|
<span className={styles.breadcrumbCurrent}>{name}</span>
|
|
</nav>
|
|
<div className={styles.headActions}>
|
|
<button
|
|
type="button"
|
|
className={`outline-btn ${styles.actionBtn}`}
|
|
disabled={!meshkeeId}
|
|
onClick={() => openBusinessDashboard("/profile")}
|
|
>
|
|
Manage business
|
|
</button>
|
|
<Link
|
|
href={`/businesses/${listingId}/edit`}
|
|
className={`outline-btn ${styles.actionBtn}`}
|
|
>
|
|
Edit listing
|
|
</Link>
|
|
</div>
|
|
</header>
|
|
|
|
<section className={styles.hero}>
|
|
{bannerUrl ? (
|
|
<img src={bannerUrl} alt="" className={styles.heroImg} />
|
|
) : (
|
|
<div className={styles.heroFallback} aria-hidden />
|
|
)}
|
|
</section>
|
|
|
|
<section className={styles.profile}>
|
|
<div className={styles.profileBar}>
|
|
<div className={styles.identity}>
|
|
{logoUrl ? (
|
|
<img className={styles.logo} src={logoUrl} alt="" height={56} />
|
|
) : (
|
|
<span className={styles.logoMark} aria-hidden>
|
|
{name.slice(0, 1).toUpperCase()}
|
|
</span>
|
|
)}
|
|
<div>
|
|
<h2 className={styles.name}>{name}</h2>
|
|
<p className={styles.tagline}>{categoryName || `/${slug}`}</p>
|
|
</div>
|
|
</div>
|
|
<div className={styles.stats}>
|
|
{sinceMonth && sinceYear ? (
|
|
<div className={styles.stat}>
|
|
<span className={styles.statLabel}>since</span>
|
|
<strong className={styles.statValue}>{sinceYear}</strong>
|
|
<span className={styles.statMonth}>{sinceMonth}</span>
|
|
</div>
|
|
) : null}
|
|
{province ? (
|
|
<div className={styles.stat}>
|
|
<span className={styles.statLabel}>province</span>
|
|
<strong className={styles.statValue}>{province}</strong>
|
|
</div>
|
|
) : null}
|
|
<div className={styles.statWide}>
|
|
<strong className={styles.statValueLg}>{portfolioTotal}</strong>
|
|
<span className={styles.statLabelStack}>
|
|
active
|
|
<br />
|
|
{portfolioTotal === 1 ? "portfolio" : "portfolios"}
|
|
</span>
|
|
</div>
|
|
{telephone ? (
|
|
<div className={styles.stat}>
|
|
<span className={styles.statLabel}>Telephone</span>
|
|
<strong className={styles.statValue}>{telephone}</strong>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
{description ? (
|
|
<p className={styles.description} dir="auto">
|
|
{description}
|
|
</p>
|
|
) : null}
|
|
</section>
|
|
|
|
{availableTabs.length && meshkeeId ? (
|
|
<>
|
|
<nav className={styles.tabs} aria-label="Business sections">
|
|
{availableTabs.map((id) => (
|
|
<button
|
|
key={id}
|
|
type="button"
|
|
className={tab === id ? styles.tabActive : styles.tab}
|
|
onClick={() => setTab(id)}
|
|
>
|
|
{TAB_LABELS[id]}
|
|
</button>
|
|
))}
|
|
</nav>
|
|
|
|
<section className={styles.panel} aria-live="polite">
|
|
{tab === "products" ? (
|
|
<ProductsPanel
|
|
meshkeeId={meshkeeId}
|
|
controlsMode="manage"
|
|
onManage={() => openBusinessDashboard("/products")}
|
|
/>
|
|
) : null}
|
|
|
|
{tab === "contact" ? (
|
|
<div className={styles.contact}>
|
|
<div className={styles.contactRows}>
|
|
{addresses.map((address, index) => (
|
|
<div
|
|
key={address.id ?? `${address.city}-${index}`}
|
|
className={styles.contactRow}
|
|
>
|
|
<span className={styles.contactLabel}>
|
|
{address.name || "Address"}
|
|
</span>
|
|
<p dir="auto">
|
|
{[address.address, address.city, address.province]
|
|
.filter(Boolean)
|
|
.join(", ")}
|
|
</p>
|
|
{address.postalCode ? (
|
|
<p>Postal: {address.postalCode}</p>
|
|
) : null}
|
|
{address.landline ? (
|
|
<p>Landline: {address.landline}</p>
|
|
) : null}
|
|
</div>
|
|
))}
|
|
{phones.length ? (
|
|
<div className={styles.contactRow}>
|
|
<span className={styles.contactLabel}>Phones</span>
|
|
<p>
|
|
{phones
|
|
.filter((item) => item.number.trim())
|
|
.map((item) => item.number)
|
|
.join(" · ")}
|
|
</p>
|
|
</div>
|
|
) : null}
|
|
{emails.length ? (
|
|
<div className={styles.contactRow}>
|
|
<span className={styles.contactLabel}>Email</span>
|
|
<p>{emails.join(" · ")}</p>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
{socials.length ? (
|
|
<div className={styles.socials}>
|
|
{socials.map((social) => (
|
|
<span key={social.network} className={styles.socialChip}>
|
|
<img src={social.icon} alt="" width={16} height={16} />
|
|
<em>{social.network}</em> {social.handle}
|
|
</span>
|
|
))}
|
|
</div>
|
|
) : null}
|
|
<button
|
|
type="button"
|
|
className={`outline-btn ${styles.actionBtn}`}
|
|
style={{ marginTop: 18 }}
|
|
onClick={() => openBusinessDashboard("/profile")}
|
|
>
|
|
Manage business
|
|
</button>
|
|
</div>
|
|
) : null}
|
|
|
|
{tab === "blog" ? (
|
|
<BlogPanel
|
|
meshkeeId={meshkeeId}
|
|
controlsMode="manage"
|
|
onManage={() => openBusinessDashboard("/blog")}
|
|
/>
|
|
) : null}
|
|
{tab === "portfolios" ? (
|
|
<PortfoliosPanel
|
|
meshkeeId={meshkeeId}
|
|
controlsMode="manage"
|
|
onManage={() => openBusinessDashboard("/portfolios")}
|
|
/>
|
|
) : null}
|
|
</section>
|
|
</>
|
|
) : (
|
|
<div className={styles.emptyTabs}>
|
|
No catalog tabs yet — products, blog, portfolios, or contact data will
|
|
appear here when Meshkee has them.
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|