Files
spsays-training/components/social/comment/comment-menu.tsx
T
2026-06-13 02:24:42 +05:30

145 lines
4.0 KiB
TypeScript

"use client";
/**
* CommentMenu — the kebab (⋮) overflow menu on a comment: Pin / Report /
* Delete (each shown only when permitted). Rendered in a portal with a
* transparent full-screen backdrop that swallows the dismiss click, so it
* can't reach (and accidentally close) the surrounding vaul comments drawer.
*/
import { useRef, useState } from "react";
import { createPortal } from "react-dom";
import { Flag, MoreVertical, Pin, PinOff, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
export function CommentMenu({
canPin,
isPinned,
canDelete,
canReport,
onPin,
onDelete,
onReport,
}: {
canPin: boolean;
isPinned: boolean;
canDelete: boolean;
canReport: boolean;
onPin: () => void;
onDelete: () => void;
onReport: () => void;
}) {
const [open, setOpen] = useState(false);
const [pos, setPos] = useState<{ top: number; right: number } | null>(null);
const btnRef = useRef<HTMLButtonElement>(null);
// Nothing actionable → no kebab at all.
if (!canPin && !canDelete && !canReport) return null;
function openMenu() {
const r = btnRef.current?.getBoundingClientRect();
if (r) setPos({ top: r.bottom + 4, right: window.innerWidth - r.right });
setOpen(true);
}
return (
<>
<button
ref={btnRef}
type="button"
onClick={openMenu}
aria-label="Comment options"
aria-haspopup="menu"
aria-expanded={open}
className="-mr-1 -mt-0.5 size-7 shrink-0 inline-flex items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
>
<MoreVertical className="size-4" />
</button>
{open && pos && typeof document !== "undefined"
? createPortal(
<>
<div
className="fixed inset-0 z-[100]"
aria-hidden
onClick={(e) => {
e.stopPropagation();
setOpen(false);
}}
/>
<div
role="menu"
style={{ position: "fixed", top: pos.top, right: pos.right }}
className="z-[101] min-w-40 rounded-lg border border-border bg-card shadow-lg py-1 animate-in fade-in-0 zoom-in-95"
>
{canPin ? (
<MenuItem
icon={isPinned ? <PinOff className="size-4" /> : <Pin className="size-4" />}
onClick={() => {
setOpen(false);
onPin();
}}
>
{isPinned ? "Unpin" : "Pin"}
</MenuItem>
) : null}
{canReport ? (
<MenuItem
icon={<Flag className="size-4" />}
onClick={() => {
setOpen(false);
onReport();
}}
>
Report
</MenuItem>
) : null}
{canDelete ? (
<MenuItem
icon={<Trash2 className="size-4" />}
danger
onClick={() => {
setOpen(false);
onDelete();
}}
>
Delete
</MenuItem>
) : null}
</div>
</>,
document.body,
)
: null}
</>
);
}
function MenuItem({
icon,
children,
onClick,
danger = false,
}: {
icon: React.ReactNode;
children: React.ReactNode;
onClick: () => void;
danger?: boolean;
}) {
return (
<button
type="button"
role="menuitem"
onClick={onClick}
className={cn(
"flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm transition-colors",
danger
? "text-destructive hover:bg-destructive/10"
: "text-foreground hover:bg-muted",
)}
>
<span className="shrink-0">{icon}</span>
{children}
</button>
);
}