Initial commit: dashboard
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useId, useState } from "react";
|
||||
import {
|
||||
getSpecialBusinesses,
|
||||
saveSpecialBusinesses,
|
||||
type SpecialCarousel,
|
||||
} from "@/services/special-businesses";
|
||||
import type { MarketplaceBusiness } from "@/types/business";
|
||||
import styles from "./MakeSpecialModal.module.css";
|
||||
|
||||
export default function MakeSpecialModal({
|
||||
business,
|
||||
open,
|
||||
onClose,
|
||||
onSaved,
|
||||
}: {
|
||||
business: MarketplaceBusiness | null;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSaved?: () => void;
|
||||
}) {
|
||||
const titleId = useId();
|
||||
const [carousels, setCarousels] = useState<SpecialCarousel[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [savingId, setSavingId] = useState<string | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
const [status, setStatus] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
setStatus("");
|
||||
try {
|
||||
const config = await getSpecialBusinesses();
|
||||
if (!cancelled) setCarousels(config.carousels);
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: "Could not load carousels.",
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const previousOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = "hidden";
|
||||
const onKey = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape" && !savingId) onClose();
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => {
|
||||
document.body.style.overflow = previousOverflow;
|
||||
window.removeEventListener("keydown", onKey);
|
||||
};
|
||||
}, [open, savingId, onClose]);
|
||||
|
||||
if (!open || !business) return null;
|
||||
|
||||
const name = business.name || business.slug;
|
||||
|
||||
async function pickCarousel(carousel: SpecialCarousel) {
|
||||
if (!business) return;
|
||||
setSavingId(carousel.id);
|
||||
setError("");
|
||||
setStatus("");
|
||||
try {
|
||||
const config = await getSpecialBusinesses();
|
||||
const nextCarousels = config.carousels.map((item) => {
|
||||
if (item.id !== carousel.id) return item;
|
||||
if (item.businessIds.includes(business.id)) return item;
|
||||
return {
|
||||
...item,
|
||||
businessIds: [...item.businessIds, business.id],
|
||||
};
|
||||
});
|
||||
await saveSpecialBusinesses({ carousels: nextCarousels });
|
||||
setStatus(`Added to ${carousel.title}`);
|
||||
onSaved?.();
|
||||
window.setTimeout(() => onClose(), 500);
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: "Could not add business to carousel.",
|
||||
);
|
||||
} finally {
|
||||
setSavingId(null);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.overlay}
|
||||
role="presentation"
|
||||
onMouseDown={(event) => {
|
||||
if (event.target === event.currentTarget && !savingId) onClose();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={styles.modal}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby={titleId}
|
||||
>
|
||||
<div className={styles.head}>
|
||||
<div>
|
||||
<p className={styles.kicker}>Make special</p>
|
||||
<h2 id={titleId} className={styles.title}>
|
||||
Choose a carousel
|
||||
</h2>
|
||||
<p className={styles.hint}>Add {name} to one of the homepage carousels.</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.closeBtn}
|
||||
aria-label="Close"
|
||||
disabled={Boolean(savingId)}
|
||||
onClick={onClose}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<p className={styles.empty}>Loading carousels…</p>
|
||||
) : error && !carousels.length ? (
|
||||
<p className={styles.error} role="alert">
|
||||
{error}
|
||||
</p>
|
||||
) : carousels.length ? (
|
||||
<ul className={styles.list}>
|
||||
{carousels.map((carousel) => {
|
||||
const alreadyIn = carousel.businessIds.includes(business.id);
|
||||
return (
|
||||
<li key={carousel.id}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.item}
|
||||
disabled={Boolean(savingId) || alreadyIn}
|
||||
onClick={() => void pickCarousel(carousel)}
|
||||
>
|
||||
<span className={styles.itemMain}>
|
||||
<span className={styles.itemTitle}>
|
||||
{carousel.title || "Untitled"}
|
||||
</span>
|
||||
{carousel.subtext ? (
|
||||
<span className={styles.itemSubtext}>
|
||||
{carousel.subtext}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
<span className={styles.itemMeta}>
|
||||
{savingId === carousel.id
|
||||
? "Adding…"
|
||||
: alreadyIn
|
||||
? "Already in"
|
||||
: `${carousel.businessIds.length} businesses`}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
) : (
|
||||
<p className={styles.empty}>
|
||||
No carousels yet. Create one under Website → Carousels.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{error && carousels.length ? (
|
||||
<p className={styles.error} role="alert">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
{status ? <p className={styles.status}>{status}</p> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user