Update carousels, card hover, and IranBay branding.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,332 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useAuth } from "@/components/AuthProvider";
|
||||
import { listBusinesses } from "@/services/marketplace";
|
||||
import {
|
||||
getMeshkeeBusiness,
|
||||
} from "@/services/meshkee-businesses";
|
||||
import { getFirstName } from "@/lib/userName";
|
||||
import type { MarketplaceBusiness } from "@/types/business";
|
||||
import styles from "./HomeDashboard.module.css";
|
||||
|
||||
type RecentRow = {
|
||||
id: string;
|
||||
name: string;
|
||||
category: string;
|
||||
status: string;
|
||||
statusKey: MarketplaceBusiness["status"];
|
||||
};
|
||||
|
||||
const STATUS_LABEL: Record<MarketplaceBusiness["status"], string> = {
|
||||
draft: "Draft",
|
||||
pending_review: "Review",
|
||||
published: "Published",
|
||||
hidden: "Hidden",
|
||||
rejected: "Rejected",
|
||||
};
|
||||
|
||||
export default function HomeDashboard() {
|
||||
const { user } = useAuth();
|
||||
const firstName = getFirstName(user);
|
||||
const [items, setItems] = useState<MarketplaceBusiness[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const data = await listBusinesses({ status: "all", limit: 100 });
|
||||
if (cancelled) return;
|
||||
|
||||
const sorted = [...data.items].sort((a, b) => {
|
||||
const aTime = Date.parse(a.updatedAt || a.createdAt || "") || 0;
|
||||
const bTime = Date.parse(b.updatedAt || b.createdAt || "") || 0;
|
||||
return bTime - aTime;
|
||||
});
|
||||
const pending = data.items.filter(
|
||||
(item) => item.status === "pending_review",
|
||||
);
|
||||
const enrichIds = new Set(
|
||||
[
|
||||
...sorted.slice(0, 4).map((item) => item.id),
|
||||
...pending.slice(0, 4).map((item) => item.id),
|
||||
],
|
||||
);
|
||||
|
||||
const enrichedEntries = await Promise.all(
|
||||
data.items
|
||||
.filter((item) => enrichIds.has(item.id))
|
||||
.map(async (item) => {
|
||||
try {
|
||||
const meshkee = await getMeshkeeBusiness(
|
||||
String(item.meshkeeBusinessId),
|
||||
);
|
||||
return [
|
||||
item.id,
|
||||
{
|
||||
...item,
|
||||
name: meshkee.name,
|
||||
categoryName: meshkee.categories[0]?.name,
|
||||
},
|
||||
] as const;
|
||||
} catch {
|
||||
return [item.id, { ...item, name: item.slug }] as const;
|
||||
}
|
||||
}),
|
||||
);
|
||||
const byId = new Map(enrichedEntries);
|
||||
setItems(data.items.map((item) => byId.get(item.id) ?? item));
|
||||
} catch (err) {
|
||||
if (cancelled) return;
|
||||
setError(
|
||||
err instanceof Error ? err.message : "Could not load marketplace data.",
|
||||
);
|
||||
setItems([]);
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const stats = useMemo(() => {
|
||||
const published = items.filter((item) => item.status === "published").length;
|
||||
const pending = items.filter(
|
||||
(item) => item.status === "pending_review",
|
||||
).length;
|
||||
const draft = items.filter((item) => item.status === "draft").length;
|
||||
return [
|
||||
{
|
||||
id: "businesses",
|
||||
label: "Live",
|
||||
value: loading ? "…" : String(published),
|
||||
hint: "Published on marketplace",
|
||||
href: "/businesses?status=published",
|
||||
},
|
||||
{
|
||||
id: "pending",
|
||||
label: "Pending",
|
||||
value: loading ? "…" : String(pending),
|
||||
hint: "Awaiting review",
|
||||
href: "/businesses?status=pending_review",
|
||||
},
|
||||
{
|
||||
id: "draft",
|
||||
label: "Draft",
|
||||
value: loading ? "…" : String(draft),
|
||||
hint: "Not submitted yet",
|
||||
href: "/businesses?status=draft",
|
||||
},
|
||||
{
|
||||
id: "total",
|
||||
label: "Total",
|
||||
value: loading ? "…" : String(items.length),
|
||||
hint: "All linked listings",
|
||||
href: "/businesses",
|
||||
},
|
||||
];
|
||||
}, [items, loading]);
|
||||
|
||||
const recent: RecentRow[] = useMemo(
|
||||
() =>
|
||||
[...items]
|
||||
.sort((a, b) => {
|
||||
const aTime = Date.parse(a.updatedAt || a.createdAt || "") || 0;
|
||||
const bTime = Date.parse(b.updatedAt || b.createdAt || "") || 0;
|
||||
return bTime - aTime;
|
||||
})
|
||||
.slice(0, 4)
|
||||
.map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name || item.slug,
|
||||
category: item.categoryName || "Uncategorized",
|
||||
status: STATUS_LABEL[item.status],
|
||||
statusKey: item.status,
|
||||
})),
|
||||
[items],
|
||||
);
|
||||
|
||||
const pendingItems = useMemo(
|
||||
() => items.filter((item) => item.status === "pending_review").slice(0, 4),
|
||||
[items],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<section className={styles.hero} aria-label="Welcome">
|
||||
<div className={styles.heroCopy}>
|
||||
<p className={styles.kicker}>Overview · {firstName}</p>
|
||||
<h1 className={styles.title}>Marketplace command</h1>
|
||||
<p className={styles.lead}>
|
||||
Track listings, review submissions, and keep the public marketplace
|
||||
gate in sync.
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.heroPanel} aria-hidden>
|
||||
<span>IranBay</span>
|
||||
<strong>Admin</strong>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={styles.stats} aria-label="Key metrics">
|
||||
{stats.map((stat) => (
|
||||
<a
|
||||
key={stat.id}
|
||||
href={stat.href}
|
||||
className={`tile-hover ${styles.statTile}`}
|
||||
>
|
||||
<p className={styles.statLabel}>{stat.label}</p>
|
||||
<p className={styles.statValue}>{stat.value}</p>
|
||||
<p className={styles.statHint}>{stat.hint}</p>
|
||||
</a>
|
||||
))}
|
||||
</section>
|
||||
|
||||
{error ? (
|
||||
<p className={styles.errorNote} role="alert">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<section className={styles.split} id="businesses">
|
||||
<div className={styles.panel}>
|
||||
<div className={styles.panelHead}>
|
||||
<div>
|
||||
<h2 className={styles.panelTitle}>Recent businesses</h2>
|
||||
<p className={styles.panelSub}>Latest marketplace listings</p>
|
||||
</div>
|
||||
<a href="/businesses" className={`outline-btn ${styles.panelBtn}`}>
|
||||
View all
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<ul className={styles.list}>
|
||||
{loading && recent.length === 0 ? (
|
||||
<li className={styles.listRow}>
|
||||
<p className={styles.rowMeta}>Loading…</p>
|
||||
</li>
|
||||
) : null}
|
||||
{!loading && recent.length === 0 ? (
|
||||
<li className={styles.listRow}>
|
||||
<p className={styles.rowMeta}>No listings yet</p>
|
||||
</li>
|
||||
) : null}
|
||||
{recent.map((item) => (
|
||||
<li key={item.id} className={styles.listRow}>
|
||||
<div>
|
||||
<p className={styles.rowTitle}>{item.name}</p>
|
||||
<p className={styles.rowMeta}>{item.category}</p>
|
||||
</div>
|
||||
<div className={styles.rowSide}>
|
||||
<span
|
||||
className={`${styles.status} ${
|
||||
item.statusKey === "published"
|
||||
? styles.statusLive
|
||||
: item.statusKey === "pending_review"
|
||||
? styles.statusReview
|
||||
: styles.statusDraft
|
||||
}`}
|
||||
>
|
||||
{item.status}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className={styles.panel} id="activity">
|
||||
<div className={styles.panelHead}>
|
||||
<div>
|
||||
<h2 className={styles.panelTitle}>Pending review</h2>
|
||||
<p className={styles.panelSub}>Approve new shop submissions</p>
|
||||
</div>
|
||||
<a
|
||||
href="/businesses?status=pending_review"
|
||||
className={`outline-btn ${styles.panelBtn}`}
|
||||
>
|
||||
Review
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<ul className={styles.activity}>
|
||||
{loading && pendingItems.length === 0 ? (
|
||||
<li className={styles.activityRow}>
|
||||
<div className={styles.activityDot} aria-hidden />
|
||||
<div>
|
||||
<p className={styles.rowMeta}>Loading…</p>
|
||||
</div>
|
||||
</li>
|
||||
) : null}
|
||||
{!loading && pendingItems.length === 0 ? (
|
||||
<li className={styles.activityRow}>
|
||||
<div className={styles.activityDot} aria-hidden />
|
||||
<div>
|
||||
<p className={styles.rowTitle}>Queue clear</p>
|
||||
<p className={styles.rowMeta}>No submissions waiting</p>
|
||||
</div>
|
||||
</li>
|
||||
) : null}
|
||||
{pendingItems.map((item) => (
|
||||
<li key={item.id} className={styles.activityRow}>
|
||||
<div className={styles.activityDot} aria-hidden />
|
||||
<div>
|
||||
<p className={styles.rowTitle}>
|
||||
{item.name || item.slug}
|
||||
</p>
|
||||
<p className={styles.rowMeta}>
|
||||
/{item.slug} · awaiting approval
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
className={styles.rowTime}
|
||||
href={`/businesses/${item.id}`}
|
||||
>
|
||||
Open
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={styles.actions} id="settings" aria-label="Quick actions">
|
||||
<div className={styles.actionsHead}>
|
||||
<h2 className={styles.panelTitle}>Quick actions</h2>
|
||||
<p className={styles.panelSub}>Jump into common admin work</p>
|
||||
</div>
|
||||
<div className={styles.actionGrid}>
|
||||
<a
|
||||
href="/businesses?status=pending_review"
|
||||
className={`tile-hover ${styles.actionTile}`}
|
||||
>
|
||||
<p className={styles.actionLabel}>Review pending</p>
|
||||
<p className={styles.actionDesc}>Approve new marketplace listings</p>
|
||||
</a>
|
||||
<a href="/businesses" className={`tile-hover ${styles.actionTile}`}>
|
||||
<p className={styles.actionLabel}>Manage businesses</p>
|
||||
<p className={styles.actionDesc}>List, edit, publish, or hide</p>
|
||||
</a>
|
||||
<a
|
||||
href={
|
||||
process.env.NEXT_PUBLIC_WEBSITE_URL?.replace(/\/$/, "") ||
|
||||
"http://localhost:3000"
|
||||
}
|
||||
className={`tile-hover ${styles.actionTile}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<p className={styles.actionLabel}>Open website</p>
|
||||
<p className={styles.actionDesc}>Preview the public gate</p>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user