Update carousels, card hover, and IranBay branding.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-10-04 13:21:27 +03:30
co-authored by Cursor
commit 1153e8e78f
178 changed files with 30517 additions and 0 deletions
+551
View File
@@ -0,0 +1,551 @@
"use client";
import { useCallback, useEffect, useId, useMemo, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import AddBusinessModal from "@/components/AddBusinessModal";
import BusinessCardList from "@/components/BusinessCardList";
import CategoryTreeSelect from "@/components/CategoryTreeSelect";
import ConfirmDialog from "@/components/ConfirmDialog";
import MakeSpecialModal from "@/components/MakeSpecialModal";
import {
deleteMarketplaceBusiness,
listBusinesses,
updateMarketplaceBusiness,
} from "@/services/marketplace";
import {
getBusinessProfile,
getMeshkeeBusiness,
} from "@/services/meshkee-businesses";
import {
categoryLabel,
searchMeshkeeCategories,
type MeshkeeCategory,
} from "@/services/meshkee-categories";
import type { BusinessStatus, MarketplaceBusiness } from "@/types/business";
import styles from "./BusinessesPage.module.css";
type FilterKey = "all" | "pending_review" | "published" | "rejected";
const FILTERS: { key: FilterKey; label: string }[] = [
{ key: "all", label: "All" },
{ key: "pending_review", label: "Pending" },
{ key: "published", label: "Published" },
{ key: "rejected", label: "Rejected" },
];
function normalizeStatusParam(value: string | null): FilterKey {
if (!value) return "all";
if (value === "draft" || value === "pending_review") return "pending_review";
if (value === "published" || value === "rejected" || value === "all") {
return value;
}
return "all";
}
function isPendingStatus(status: BusinessStatus) {
return status === "pending_review" || status === "draft";
}
function categoryAncestors(
categoryId: string | undefined,
byId: Map<string, MeshkeeCategory>,
): string[] {
if (!categoryId) return [];
const names: string[] = [];
const seen = new Set<string>();
let current = byId.get(categoryId);
while (current && !seen.has(current.id)) {
seen.add(current.id);
names.unshift(current.name);
current = current.parentId ? byId.get(current.parentId) : undefined;
}
return names;
}
function lastTwoCategoryLevels(trail: string[]) {
if (trail.length === 0) return undefined;
return trail.slice(-2).join(" / ");
}
function businessMatchesCategory(
categoryId: string | undefined,
selectedId: string,
byId: Map<string, MeshkeeCategory>,
) {
if (!categoryId) return false;
const seen = new Set<string>();
let current = byId.get(categoryId);
while (current && !seen.has(current.id)) {
if (current.id === selectedId) return true;
seen.add(current.id);
current = current.parentId ? byId.get(current.parentId) : undefined;
}
return false;
}
async function enrichListings(
items: MarketplaceBusiness[],
categories: MeshkeeCategory[],
): Promise<MarketplaceBusiness[]> {
const byId = new Map(categories.map((category) => [category.id, category]));
return Promise.all(
items.map(async (item) => {
try {
const id = String(item.meshkeeBusinessId);
const [meshkee, profile] = await Promise.all([
getMeshkeeBusiness(id),
getBusinessProfile(id),
]);
const primaryCategoryId =
profile.profile.categoryIds[0] ?? meshkee.categories[0]?.id;
const trail = categoryAncestors(primaryCategoryId, byId);
return {
...item,
name: meshkee.name,
categoryId: primaryCategoryId,
categoryName: trail[trail.length - 1] ?? meshkee.categories[0]?.name,
categoryTrail: lastTwoCategoryLevels(trail),
logoUrl: profile.profile.logoUrl ?? meshkee.logoUrl ?? undefined,
bannerUrl:
profile.profile.bannerUrl ?? meshkee.bannerUrl ?? undefined,
};
} catch {
return { ...item, name: item.slug };
}
}),
);
}
export default function BusinessesPage() {
const router = useRouter();
const searchParams = useSearchParams();
const filterTitleId = useId();
const initialFilter = normalizeStatusParam(searchParams.get("status"));
const [filter, setFilter] = useState<FilterKey>(initialFilter);
const [items, setItems] = useState<MarketplaceBusiness[]>([]);
const [categories, setCategories] = useState<MeshkeeCategory[]>([]);
const [total, setTotal] = useState(0);
const [error, setError] = useState("");
const [loading, setLoading] = useState(true);
const [modalOpen, setModalOpen] = useState(false);
const [filterModalOpen, setFilterModalOpen] = useState(false);
const [draftCategoryIds, setDraftCategoryIds] = useState<string[]>([]);
const [categoryFilterId, setCategoryFilterId] = useState<string | null>(null);
const [removing, setRemoving] = useState<MarketplaceBusiness | null>(null);
const [makingSpecial, setMakingSpecial] =
useState<MarketplaceBusiness | null>(null);
const [specialTick, setSpecialTick] = useState(0);
const [busyId, setBusyId] = useState<string | null>(null);
const categoriesById = useMemo(
() => new Map(categories.map((category) => [category.id, category])),
[categories],
);
const activeCategory = categoryFilterId
? categoriesById.get(categoryFilterId)
: undefined;
const activeFilterCount = categoryFilterId ? 1 : 0;
const reload = useCallback(async () => {
setLoading(true);
setError("");
try {
const [data, categoryItems] = await Promise.all([
listBusinesses({ status: "all", limit: 100 }),
searchMeshkeeCategories("").catch(() => [] as MeshkeeCategory[]),
]);
setCategories(categoryItems);
const enriched = await enrichListings(data.items, categoryItems);
setItems(enriched);
setTotal(data.total);
} catch (err) {
setError(
err instanceof Error
? err.message
: "Could not load businesses from the API.",
);
setItems([]);
setTotal(0);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
void reload();
}, [reload]);
useEffect(() => {
setFilter(normalizeStatusParam(searchParams.get("status")));
}, [searchParams]);
useEffect(() => {
if (!filterModalOpen) return;
const previousOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
const onKey = (event: KeyboardEvent) => {
if (event.key === "Escape") setFilterModalOpen(false);
};
window.addEventListener("keydown", onKey);
return () => {
document.body.style.overflow = previousOverflow;
window.removeEventListener("keydown", onKey);
};
}, [filterModalOpen]);
const counts = useMemo(() => {
const next: Record<FilterKey, number> = {
all: 0,
pending_review: 0,
published: 0,
rejected: 0,
};
for (const item of items) {
if (
categoryFilterId &&
!businessMatchesCategory(
item.categoryId,
categoryFilterId,
categoriesById,
)
) {
continue;
}
next.all += 1;
if (isPendingStatus(item.status)) next.pending_review += 1;
else if (item.status === "published") next.published += 1;
else if (item.status === "rejected") next.rejected += 1;
}
return next;
}, [items, categoryFilterId, categoriesById]);
const visible = useMemo(() => {
return items.filter((item) => {
if (
categoryFilterId &&
!businessMatchesCategory(
item.categoryId,
categoryFilterId,
categoriesById,
)
) {
return false;
}
if (filter === "all") return true;
if (filter === "pending_review") return isPendingStatus(item.status);
return item.status === filter;
});
}, [items, filter, categoryFilterId, categoriesById]);
function selectFilter(next: FilterKey) {
setFilter(next);
const url = new URL(window.location.href);
if (next === "all") url.searchParams.delete("status");
else url.searchParams.set("status", next);
router.replace(`${url.pathname}${url.search}`);
}
function openFilterModal() {
setDraftCategoryIds(categoryFilterId ? [categoryFilterId] : []);
setFilterModalOpen(true);
}
function applyCategoryFilter() {
setCategoryFilterId(draftCategoryIds[0] ?? null);
setFilterModalOpen(false);
}
function clearCategoryFilter() {
setCategoryFilterId(null);
setDraftCategoryIds([]);
}
async function setStatus(business: MarketplaceBusiness, status: BusinessStatus) {
setBusyId(business.id);
setError("");
try {
const updated = await updateMarketplaceBusiness(business.id, { status });
setItems((current) =>
current.map((item) =>
item.id === business.id
? {
...item,
status: updated.status,
isFeatured: updated.isFeatured ?? item.isFeatured,
sortWeight: updated.sortWeight ?? item.sortWeight,
slug: updated.slug || item.slug,
}
: item,
),
);
} catch (err) {
setError(
err instanceof Error
? err.message
: "Could not update listing status.",
);
} finally {
setBusyId(null);
}
}
const removingBusy = Boolean(removing && busyId === removing.id);
const removingName = removing?.name || removing?.slug || "this business";
return (
<div className={styles.page}>
<header className={styles.head}>
<div>
<nav className={styles.breadcrumb} aria-label="Breadcrumb">
<a href="/">Home</a>
<span className={styles.breadcrumbSep} aria-hidden>
/
</span>
<span className={styles.breadcrumbCurrent}>Businesses</span>
</nav>
<h1 className={styles.title}>Businesses</h1>
<p className={styles.lead}>
Review submissions, publish listings, and manage the marketplace
gate.
</p>
</div>
<div className={styles.headActions}>
<p className={styles.count}>
{loading
? "Loading…"
: filter === "all" && !categoryFilterId
? `${total} total`
: `${visible.length} shown · ${total} total`}
</p>
<button
type="button"
className={`${styles.filterOpenBtn}${categoryFilterId ? ` ${styles.filterOpenBtnActive}` : ""}`}
onClick={openFilterModal}
aria-label={
categoryFilterId
? `Filter, ${activeFilterCount} active`
: "Filter"
}
>
<span>Filter</span>
{activeFilterCount > 0 ? (
<em className={styles.filterCount}>{activeFilterCount}</em>
) : null}
</button>
<button
type="button"
className={styles.addBtn}
aria-label="Add business"
onClick={() => setModalOpen(true)}
>
<img src="/images/icons/add.svg" alt="" width={18} height={18} />
</button>
</div>
</header>
<div className={styles.filterBar}>
{activeCategory ? (
<>
<div className={styles.activeFilters}>
<button
type="button"
className={styles.activeChip}
onClick={clearCategoryFilter}
aria-label={`Clear category filter ${categoryLabel(activeCategory)}`}
>
<span>{categoryLabel(activeCategory)}</span>
<em aria-hidden>×</em>
</button>
</div>
<div className={styles.filterDivider} aria-hidden />
</>
) : null}
<div className={styles.filters} role="tablist" aria-label="Listing status">
{FILTERS.map((item) => {
const active = filter === item.key;
const count = counts[item.key];
return (
<button
key={item.key}
type="button"
role="tab"
aria-selected={active}
className={`${styles.filter} ${active ? styles.filterActive : ""}`}
onClick={() => selectFilter(item.key)}
>
<span>{item.label}</span>
<em>{loading ? "—" : count}</em>
</button>
);
})}
</div>
</div>
{error ? (
<div className={styles.error} role="alert">
<p className={styles.errorTitle}>API unavailable</p>
<p className={styles.errorCopy}>{error}</p>
<p className={styles.errorHint}>
Start the marketplace backend on port 3001, then refresh.
</p>
</div>
) : loading ? (
<div className={styles.loading}>Loading businesses…</div>
) : (
<BusinessCardList
items={visible}
busyId={busyId}
specialTick={specialTick}
emptyTitle={
filter === "pending_review"
? "No pending submissions"
: "No businesses in this view"
}
emptyCopy={
filter === "pending_review"
? "New shop registrations will appear here for approval."
: "Try another filter, or add a Meshkee business to the marketplace."
}
onApprove={(business) => void setStatus(business, "published")}
onDisable={async (business) => {
const nextStatus =
business.status === "hidden" ? "published" : "hidden";
await setStatus(business, nextStatus);
}}
onRemove={setRemoving}
onMakeSpecial={setMakingSpecial}
onMetaTag={(business) => {
const lines = [
`Name: ${business.name || business.slug}`,
`Slug: /${business.slug}`,
`Category: ${business.categoryName || "Uncategorized"}`,
`Status: ${business.status}`,
`Featured: ${business.isFeatured ? "yes" : "no"}`,
].join("\n");
window.alert(lines);
}}
/>
)}
{filterModalOpen ? (
<div
className={styles.overlay}
role="presentation"
onMouseDown={(event) => {
if (event.target === event.currentTarget) {
setFilterModalOpen(false);
}
}}
>
<div
className={styles.modal}
role="dialog"
aria-modal="true"
aria-labelledby={filterTitleId}
>
<div className={styles.modalHead}>
<div>
<p className={styles.kicker}>Businesses</p>
<h2 id={filterTitleId} className={styles.modalTitle}>
Filter
</h2>
</div>
<button
type="button"
className={styles.closeBtn}
aria-label="Close"
onClick={() => setFilterModalOpen(false)}
>
×
</button>
</div>
<form
className={styles.modalForm}
onSubmit={(event) => {
event.preventDefault();
applyCategoryFilter();
}}
>
<CategoryTreeSelect
categories={categories}
value={draftCategoryIds}
selection="single"
dropdownVariant="inFlow"
label="Category"
placeholder="Select category"
onChange={setDraftCategoryIds}
/>
<div className={styles.modalActions}>
<button
type="button"
className={`outline-btn ${styles.ghostBtn}`}
onClick={() => {
setDraftCategoryIds([]);
}}
>
Clear
</button>
<button type="submit" className={`outline-btn ${styles.applyBtn}`}>
Apply
</button>
</div>
</form>
</div>
</div>
) : null}
<AddBusinessModal
open={modalOpen}
onClose={() => setModalOpen(false)}
onConnected={() => {
void reload();
}}
/>
<MakeSpecialModal
open={Boolean(makingSpecial)}
business={makingSpecial}
onClose={() => setMakingSpecial(null)}
onSaved={() => setSpecialTick((value) => value + 1)}
/>
<ConfirmDialog
open={Boolean(removing)}
kicker="Remove listing"
title={`Remove ${removingName}?`}
message="This unlists the business from the marketplace. The Meshkee business stays intact."
confirmLabel="Remove"
cancelLabel="Keep"
danger
busy={removingBusy}
onClose={() => {
if (!removingBusy) setRemoving(null);
}}
onConfirm={async () => {
if (!removing) return;
setBusyId(removing.id);
setError("");
try {
await deleteMarketplaceBusiness(removing.id);
setRemoving(null);
await reload();
} catch (err) {
setError(
err instanceof Error
? err.message
: "Could not remove listing.",
);
} finally {
setBusyId(null);
}
}}
/>
</div>
);
}