Initial commit: dashboard

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-10-03 23:17:36 +03:30
co-authored by Cursor
commit 3db32212ad
177 changed files with 29988 additions and 0 deletions
+514
View File
@@ -0,0 +1,514 @@
"use client";
import { useEffect, useId, useMemo, useState } from "react";
import { listBusinesses } from "@/services/marketplace";
import {
getSpecialBusinesses,
newCarousel,
saveSpecialBusinesses,
slugFromTitle,
type SpecialBusinessesConfig,
type SpecialCarousel,
} from "@/services/special-businesses";
import type { MarketplaceBusiness } from "@/types/business";
import {
getBusinessProfile,
getMeshkeeBusiness,
} from "@/services/meshkee-businesses";
import styles from "./SpecialBusinessesPage.module.css";
type EnrichedBusiness = MarketplaceBusiness & {
displayName: string;
subtext: string;
};
type CarouselFormMode = "create" | "edit";
async function enrichBusinesses(
items: MarketplaceBusiness[],
): Promise<EnrichedBusiness[]> {
return Promise.all(
items.map(async (item) => {
try {
const id = String(item.meshkeeBusinessId);
const [meshkee, profile] = await Promise.all([
getMeshkeeBusiness(id),
getBusinessProfile(id),
]);
return {
...item,
name: meshkee.name,
displayName:
profile.profile.nameEn ||
profile.profile.nameFa ||
meshkee.name ||
item.slug,
subtext: (profile.profile.title || "").trim(),
logoUrl: profile.profile.logoUrl ?? meshkee.logoUrl ?? undefined,
};
} catch {
return {
...item,
displayName: item.name || item.slug,
subtext: "",
};
}
}),
);
}
function BusinessIdentity({ item }: { item: EnrichedBusiness }) {
return (
<div className={styles.identity}>
{item.logoUrl ? (
<img className={styles.logo} src={item.logoUrl} alt="" height={44} />
) : (
<span className={styles.logoMark} aria-hidden>
{item.displayName.slice(0, 1).toUpperCase()}
</span>
)}
<div className={styles.listMain}>
<span className={styles.listName}>{item.displayName}</span>
{item.subtext ? (
<span className={styles.listSubtext}>{item.subtext}</span>
) : (
<span className={styles.listSlug}>/{item.slug}</span>
)}
</div>
</div>
);
}
export default function SpecialBusinessesPage() {
const [config, setConfig] = useState<SpecialBusinessesConfig | null>(null);
const [activeId, setActiveId] = useState("");
const [allBusinesses, setAllBusinesses] = useState<EnrichedBusiness[]>([]);
const [query, setQuery] = useState("");
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [status, setStatus] = useState("");
const [error, setError] = useState("");
const [formMode, setFormMode] = useState<CarouselFormMode | null>(null);
const [formName, setFormName] = useState("");
const [formSubtext, setFormSubtext] = useState("");
const [formError, setFormError] = useState("");
const formTitleId = useId();
useEffect(() => {
let cancelled = false;
void (async () => {
setLoading(true);
setError("");
try {
const [special, listings] = await Promise.all([
getSpecialBusinesses(),
listBusinesses({ status: "all", limit: 200 }),
]);
const enriched = await enrichBusinesses(listings.items);
if (cancelled) return;
setConfig(special);
setActiveId(special.carousels[0]?.id ?? "");
setAllBusinesses(enriched);
} catch (err) {
if (!cancelled) {
setError(
err instanceof Error
? err.message
: "Could not load special businesses.",
);
}
} finally {
if (!cancelled) setLoading(false);
}
})();
return () => {
cancelled = true;
};
}, []);
const active = useMemo(
() => config?.carousels.find((item) => item.id === activeId) ?? null,
[config, activeId],
);
const byId = useMemo(() => {
const map = new Map<string, EnrichedBusiness>();
for (const item of allBusinesses) map.set(item.id, item);
return map;
}, [allBusinesses]);
const selectedBusinesses = useMemo(() => {
if (!active) return [];
return active.businessIds
.map((id) => byId.get(id))
.filter(Boolean) as EnrichedBusiness[];
}, [active, byId]);
const searchResults = useMemo(() => {
if (!active) return [];
const q = query.trim().toLowerCase();
if (q.length <= 2) return [];
const selected = new Set(active.businessIds);
return allBusinesses
.filter((item) => !selected.has(item.id))
.filter(
(item) =>
item.displayName.toLowerCase().includes(q) ||
item.subtext.toLowerCase().includes(q) ||
item.slug.toLowerCase().includes(q) ||
item.id.includes(q),
)
.slice(0, 12);
}, [active, allBusinesses, query]);
function updateCarousel(id: string, patch: Partial<SpecialCarousel>) {
setConfig((prev) => {
if (!prev) return prev;
return {
carousels: prev.carousels.map((item) =>
item.id === id ? { ...item, ...patch } : item,
),
};
});
setStatus("");
}
function openCreateModal() {
setFormMode("create");
setFormName("");
setFormSubtext("");
setFormError("");
}
function openEditModal() {
if (!active) return;
setFormMode("edit");
setFormName(active.title);
setFormSubtext(active.subtext || "");
setFormError("");
}
function closeFormModal() {
setFormMode(null);
setFormError("");
}
function submitFormModal() {
const title = formName.trim();
if (!title) {
setFormError("Name is required.");
return;
}
const subtext = formSubtext.trim();
if (formMode === "create") {
const next = newCarousel(title, subtext);
setConfig((prev) =>
prev
? { carousels: [...prev.carousels, next] }
: { carousels: [next] },
);
setActiveId(next.id);
setStatus("");
closeFormModal();
return;
}
if (formMode === "edit" && active) {
updateCarousel(active.id, {
title,
subtext,
slug: slugFromTitle(title),
});
closeFormModal();
}
}
function removeCarousel(id: string) {
setConfig((prev) => {
if (!prev || prev.carousels.length <= 1) return prev;
const carousels = prev.carousels.filter((item) => item.id !== id);
if (activeId === id) setActiveId(carousels[0]?.id ?? "");
return { carousels };
});
setStatus("");
closeFormModal();
}
function addBusiness(businessId: string) {
if (!active) return;
if (active.businessIds.includes(businessId)) return;
updateCarousel(active.id, {
businessIds: [...active.businessIds, businessId],
});
setQuery("");
}
function removeBusiness(businessId: string) {
if (!active) return;
updateCarousel(active.id, {
businessIds: active.businessIds.filter((id) => id !== businessId),
});
}
async function onSave() {
if (!config) return;
setSaving(true);
setStatus("");
setError("");
try {
const saved = await saveSpecialBusinesses(config);
setConfig(saved);
if (!saved.carousels.some((item) => item.id === activeId)) {
setActiveId(saved.carousels[0]?.id ?? "");
}
setStatus("Saved");
} catch (err) {
setError(err instanceof Error ? err.message : "Could not save.");
} finally {
setSaving(false);
}
}
if (loading) {
return <div className={styles.loading}>Loading special businesses…</div>;
}
if (!config || !active) {
return (
<div className={styles.loading} role="alert">
{error || "Could not load special businesses."}
</div>
);
}
return (
<div className={styles.page}>
<header className={styles.head}>
<div>
<h1 className={styles.title}>Carousels</h1>
<p className={styles.lead}>
Homepage carousels as tabs — each tab holds multiple businesses.
</p>
</div>
<div className={styles.headActions}>
{status ? (
<span className={`${styles.status} ${styles.statusOk}`}>{status}</span>
) : null}
{error ? (
<span className={`${styles.status} ${styles.statusError}`}>
{error}
</span>
) : null}
<button
type="button"
className={`outline-btn ${styles.addBtn}`}
onClick={openEditModal}
>
Edit
</button>
<button
type="button"
className={`outline-btn ${styles.addBtn}`}
onClick={openCreateModal}
>
Add carousel
</button>
<button
type="button"
className={`outline-btn ${styles.saveBtn}`}
disabled={saving}
onClick={() => void onSave()}
>
{saving ? "Saving…" : "Save"}
</button>
</div>
</header>
<section className={styles.card}>
<div className={styles.tabs} role="tablist" aria-label="Carousels">
{config.carousels.map((carousel) => (
<button
key={carousel.id}
type="button"
role="tab"
aria-selected={carousel.id === activeId}
className={`${styles.tab} ${
carousel.id === activeId ? styles.tabActive : ""
}`}
onClick={() => setActiveId(carousel.id)}
>
{carousel.title || "Untitled"}
</button>
))}
</div>
<div className={styles.panel} role="tabpanel">
<div className={styles.activeMeta}>
<h2 className={styles.activeTitle}>{active.title}</h2>
{active.subtext ? (
<p className={styles.activeSubtext}>{active.subtext}</p>
) : null}
</div>
<div className={styles.searchRow}>
<label className={styles.field}>
<span>Search businesses</span>
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Search by name or slug"
/>
</label>
</div>
{query.trim().length > 2 ? (
searchResults.length ? (
<div className={styles.results}>
{searchResults.map((item) => (
<div key={item.id} className={styles.resultRow}>
<BusinessIdentity item={item} />
<button
type="button"
className={`outline-btn ${styles.inlineAddBtn}`}
onClick={() => addBusiness(item.id)}
>
Add
</button>
</div>
))}
</div>
) : (
<div className={styles.empty}>No matching businesses to add.</div>
)
) : null}
<div className={styles.listHead}>
<h3 className={styles.listHeading}>
Businesses in this carousel
</h3>
<span className={styles.listCount}>
{selectedBusinesses.length}
</span>
</div>
<div className={styles.list}>
{selectedBusinesses.length ? (
selectedBusinesses.map((item) => (
<div key={item.id} className={styles.listItem}>
<BusinessIdentity item={item} />
<button
type="button"
className={styles.linkBtn}
onClick={() => removeBusiness(item.id)}
>
Remove
</button>
</div>
))
) : (
<div className={styles.empty}>
No businesses in this carousel yet. Search above to add some.
</div>
)}
</div>
</div>
</section>
{formMode ? (
<div
className={styles.overlay}
role="presentation"
onMouseDown={(event) => {
if (event.target === event.currentTarget) closeFormModal();
}}
>
<div
className={styles.modal}
role="dialog"
aria-modal="true"
aria-labelledby={formTitleId}
>
<div className={styles.modalHead}>
<div>
<p className={styles.kicker}>Carousels</p>
<h2 id={formTitleId} className={styles.modalTitle}>
{formMode === "create" ? "Add carousel" : "Edit carousel"}
</h2>
</div>
<button
type="button"
className={styles.closeBtn}
aria-label="Close"
onClick={closeFormModal}
>
×
</button>
</div>
<form
className={styles.form}
onSubmit={(event) => {
event.preventDefault();
submitFormModal();
}}
>
<label className={styles.field}>
<span>Name</span>
<input
value={formName}
onChange={(event) => setFormName(event.target.value)}
placeholder="Featured producers"
autoFocus
/>
</label>
<label className={styles.field}>
<span>Subtext</span>
<input
value={formSubtext}
onChange={(event) => setFormSubtext(event.target.value)}
placeholder="Short supporting line"
/>
</label>
{formError ? (
<p className={styles.formError} role="alert">
{formError}
</p>
) : null}
<div className={styles.modalActions}>
{formMode === "edit" && config.carousels.length > 1 ? (
<button
type="button"
className={styles.dangerBtn}
onClick={() => removeCarousel(active.id)}
>
Remove carousel
</button>
) : (
<span />
)}
<div className={styles.modalActionsRight}>
<button
type="button"
className={`outline-btn ${styles.addBtn}`}
onClick={closeFormModal}
>
Cancel
</button>
<button
type="submit"
className={`outline-btn ${styles.saveBtn}`}
>
{formMode === "create" ? "Add" : "Update"}
</button>
</div>
</div>
</form>
</div>
</div>
) : null}
</div>
);
}