Files
dashboards/components/WebsiteLinkGenerator.tsx

222 lines
6.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useId, useMemo, useState } from "react";
import CategoryTreeSelect from "@/components/CategoryTreeSelect";
import {
searchMeshkeeCategories,
type MeshkeeCategory,
} from "@/services/meshkee-categories";
import styles from "./WebsiteLinkGenerator.module.css";
export type WebsiteLinkKind = "businesses" | "blog";
function buildCategoryLink(kind: WebsiteLinkKind, category: MeshkeeCategory) {
const path = kind === "blog" ? "/blog" : "/businesses";
const query = category.slug
? `category=${encodeURIComponent(category.slug)}`
: `categoryId=${encodeURIComponent(category.id)}`;
return `${path}?${query}`;
}
function LinkIcon() {
return (
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<path d="M10 13a5 5 0 0 0 7.54.54l1.92-1.92a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
<path d="M14 11a5 5 0 0 0-7.54-.54L4.54 12.38a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
);
}
export default function WebsiteLinkGenerator({
onPick,
disabled = false,
}: {
onPick: (url: string) => void;
disabled?: boolean;
}) {
const titleId = useId();
const [open, setOpen] = useState(false);
const [kind, setKind] = useState<WebsiteLinkKind>("businesses");
const [categories, setCategories] = useState<MeshkeeCategory[]>([]);
const [categoryId, setCategoryId] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
if (!open) return;
setKind("businesses");
setCategoryId("");
setError("");
}, [open]);
useEffect(() => {
if (!open) return;
const previousOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
const onKey = (event: KeyboardEvent) => {
if (event.key === "Escape") setOpen(false);
};
window.addEventListener("keydown", onKey);
return () => {
document.body.style.overflow = previousOverflow;
window.removeEventListener("keydown", onKey);
};
}, [open]);
useEffect(() => {
if (!open) return;
const controller = new AbortController();
setLoading(true);
setError("");
void searchMeshkeeCategories("", controller.signal)
.then((items) => {
if (controller.signal.aborted) return;
setCategories(items);
})
.catch((err) => {
if (controller.signal.aborted) return;
setCategories([]);
setError(
err instanceof Error ? err.message : "Could not load categories.",
);
})
.finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [open]);
const selected = useMemo(
() => categories.find((item) => item.id === categoryId) ?? null,
[categories, categoryId],
);
const preview = selected ? buildCategoryLink(kind, selected) : "";
return (
<>
<button
type="button"
className={styles.trigger}
aria-label="Generate link"
title="Generate link"
disabled={disabled}
onClick={() => setOpen(true)}
>
<LinkIcon />
</button>
{open ? (
<div
className={styles.overlay}
role="presentation"
onClick={() => setOpen(false)}
>
<div
className={styles.modal}
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
onClick={(event) => event.stopPropagation()}
>
<div className={styles.head}>
<div>
<p className={styles.kicker}>Link generator</p>
<h3 id={titleId} className={styles.title}>
Build category link
</h3>
</div>
<button
type="button"
className={styles.closeBtn}
aria-label="Close"
onClick={() => setOpen(false)}
>
×
</button>
</div>
<div className={styles.kindSwitch} role="tablist" aria-label="Link type">
<button
type="button"
role="tab"
aria-selected={kind === "businesses"}
className={`${styles.kindBtn} ${
kind === "businesses" ? styles.kindBtnActive : ""
}`}
onClick={() => setKind("businesses")}
>
Businesses category
</button>
<button
type="button"
role="tab"
aria-selected={kind === "blog"}
className={`${styles.kindBtn} ${
kind === "blog" ? styles.kindBtnActive : ""
}`}
onClick={() => setKind("blog")}
>
Blog category
</button>
</div>
{loading ? (
<p className={styles.status}>Loading categories…</p>
) : error ? (
<p className={styles.error}>{error}</p>
) : (
<CategoryTreeSelect
categories={categories}
value={categoryId ? [categoryId] : []}
onChange={(ids) => setCategoryId(ids[0] ?? "")}
selection="single"
dropdownVariant="inFlow"
label="Category"
placeholder="Search categories…"
/>
)}
<div className={styles.preview}>
<span>Preview</span>
<code dir="ltr">{preview || "Select a category"}</code>
</div>
<div className={styles.actions}>
<button
type="button"
className={`outline-btn ${styles.secondaryBtn}`}
onClick={() => setOpen(false)}
>
Cancel
</button>
<button
type="button"
className={`outline-btn ${styles.primaryBtn}`}
disabled={!preview}
onClick={() => {
if (!preview) return;
onPick(preview);
setOpen(false);
}}
>
Use link
</button>
</div>
</div>
</div>
) : null}
</>
);
}