194 lines
5.6 KiB
TypeScript
194 lines
5.6 KiB
TypeScript
"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>
|
||
);
|
||
}
|