feat: social graph features
This commit is contained in:
@@ -30,9 +30,26 @@ import {
|
||||
DrawerTitle,
|
||||
} from "@/components/ui/drawer";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Post } from "@/lib/social";
|
||||
import {
|
||||
posts,
|
||||
relationships,
|
||||
type Post,
|
||||
type PostReportCategory,
|
||||
} from "@/lib/social";
|
||||
import { useAuth } from "@/components/auth/auth-provider";
|
||||
import { useAuthGate } from "@/components/auth/auth-gate";
|
||||
import { useMediaQuery } from "@/components/selection-toolbar/shared";
|
||||
import { ConfirmModal } from "@/components/ui/confirm-modal";
|
||||
import { ReportDialog, type ReportOption } from "@/components/social/report-dialog";
|
||||
|
||||
// Post report categories — mirrors the BFF allow-list (postReportCategories).
|
||||
const POST_REPORT_OPTIONS: ReportOption[] = [
|
||||
{ value: "spam", label: "Spam or misleading" },
|
||||
{ value: "harassment", label: "Harassment or abuse" },
|
||||
{ value: "misinformation", label: "False information" },
|
||||
{ value: "inappropriate", label: "Inappropriate content" },
|
||||
{ value: "other", label: "Something else" },
|
||||
];
|
||||
|
||||
type MenuItem = {
|
||||
key: string;
|
||||
@@ -54,8 +71,11 @@ export function PostCardMenu({
|
||||
onDelete?: (id: string) => void;
|
||||
}) {
|
||||
const { user, loading } = useAuth();
|
||||
const { requireAuth } = useAuthGate();
|
||||
const isMobile = useMediaQuery("(max-width: 767px)");
|
||||
const [open, setOpen] = useState(false);
|
||||
const [blockOpen, setBlockOpen] = useState(false);
|
||||
const [reportOpen, setReportOpen] = useState(false);
|
||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||
const popoverRef = useRef<HTMLDivElement>(null);
|
||||
const [popPos, setPopPos] = useState<{ top: number; left: number } | null>(
|
||||
@@ -147,16 +167,15 @@ export function PostCardMenu({
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (isAuthed) {
|
||||
// Block needs the author's handle (the block API is keyed by handle).
|
||||
if (isAuthed && authorHandle) {
|
||||
items.push({
|
||||
key: "block",
|
||||
label: `Block ${authorDisplay}`,
|
||||
icon: <Ban className="size-4" />,
|
||||
onClick: () => {
|
||||
setOpen(false);
|
||||
toast(`Blocking ${authorDisplay} is coming soon`, {
|
||||
duration: 2500,
|
||||
});
|
||||
setBlockOpen(true);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -166,7 +185,11 @@ export function PostCardMenu({
|
||||
icon: <Flag className="size-4" />,
|
||||
onClick: () => {
|
||||
setOpen(false);
|
||||
toast("Post reporting is coming soon", { duration: 2500 });
|
||||
// Reporting requires auth (the BFF route is protected). Gate
|
||||
// anonymous viewers through the login modal, then open the form.
|
||||
void (async () => {
|
||||
if (await requireAuth()) setReportOpen(true);
|
||||
})();
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -193,54 +216,74 @@ export function PostCardMenu({
|
||||
</button>
|
||||
);
|
||||
|
||||
// Mobile: bottom drawer
|
||||
if (isMobile) {
|
||||
return (
|
||||
<>
|
||||
{trigger}
|
||||
<Drawer open={open} onOpenChange={(o) => setOpen(o)}>
|
||||
<DrawerContent className="bg-card">
|
||||
<DrawerHeader className="text-left border-b border-border pb-3">
|
||||
<DrawerTitle>Post options</DrawerTitle>
|
||||
</DrawerHeader>
|
||||
<div className="p-3 space-y-1">
|
||||
{items.map((item) => (
|
||||
<MenuItemButton key={item.key} item={item} mobile />
|
||||
))}
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
// Menu surface: bottom drawer on mobile, floating popover on desktop.
|
||||
const menu = isMobile ? (
|
||||
<Drawer open={open} onOpenChange={(o) => setOpen(o)}>
|
||||
<DrawerContent className="bg-card">
|
||||
<DrawerHeader className="text-left border-b border-border pb-3">
|
||||
<DrawerTitle>Post options</DrawerTitle>
|
||||
</DrawerHeader>
|
||||
<div className="p-3 space-y-1">
|
||||
{items.map((item) => (
|
||||
<MenuItemButton key={item.key} item={item} mobile />
|
||||
))}
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
) : open && popPos ? (
|
||||
createPortal(
|
||||
<div
|
||||
ref={popoverRef}
|
||||
role="menu"
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: popPos.top,
|
||||
left: popPos.left,
|
||||
zIndex: 60,
|
||||
}}
|
||||
className={cn(
|
||||
"w-60 rounded-lg border border-border bg-card shadow-lg py-1",
|
||||
"animate-in fade-in-0 zoom-in-95",
|
||||
)}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<MenuItemButton key={item.key} item={item} />
|
||||
))}
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
) : null;
|
||||
|
||||
// Desktop: floating popover anchored to the trigger.
|
||||
return (
|
||||
<>
|
||||
{trigger}
|
||||
{open && popPos
|
||||
? createPortal(
|
||||
<div
|
||||
ref={popoverRef}
|
||||
role="menu"
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: popPos.top,
|
||||
left: popPos.left,
|
||||
zIndex: 60,
|
||||
}}
|
||||
className={cn(
|
||||
"w-60 rounded-lg border border-border bg-card shadow-lg py-1",
|
||||
"animate-in fade-in-0 zoom-in-95",
|
||||
)}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<MenuItemButton key={item.key} item={item} />
|
||||
))}
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
: null}
|
||||
{menu}
|
||||
|
||||
{/* Block confirm — only meaningful when we know the author's handle. */}
|
||||
{authorHandle ? (
|
||||
<ConfirmModal
|
||||
open={blockOpen}
|
||||
onClose={() => setBlockOpen(false)}
|
||||
title={`Block ${authorDisplay}?`}
|
||||
message="They won't be able to follow you or see that you follow them, and any follow between you will be removed. You can unblock them later."
|
||||
confirmLabel="Block"
|
||||
onConfirm={async () => {
|
||||
await relationships.block(authorHandle);
|
||||
toast(`Blocked ${authorDisplay}`, { duration: 2500 });
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<ReportDialog
|
||||
open={reportOpen}
|
||||
onClose={() => setReportOpen(false)}
|
||||
title="Report post"
|
||||
options={POST_REPORT_OPTIONS}
|
||||
onSubmit={async (category, reason) => {
|
||||
await posts.report(post.id, category as PostReportCategory, reason);
|
||||
toast("Thanks — your report was submitted", { duration: 3000 });
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user