Initial commit: dashboard
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
"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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user