- New useDeleteEntity hook: publishes a kind:5 deletion request referencing the target addressable event by `a` coord, `e` id, and `k` kind. Owner-only. - New reusable DeleteEntityDialog: owner-gated trigger + confirmation with a clear "deletion is best-effort on decentralized relays" warning, redirect, and query invalidation so the item drops out of lists. - Wired "Disband" into CrewDetailPage (founder only, redirects to /crews). - Wired a "Manage → Delete Profile" card into CoachDetailPage for all Lab profile types (coach/filmer/editor/photographer/builder), redirects to /lab. Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
145 lines
4.9 KiB
TypeScript
145 lines
4.9 KiB
TypeScript
import { useState, type ReactNode } from 'react';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { Trash2, Loader2, AlertTriangle } from 'lucide-react';
|
||
import { Button } from '@/components/ui/button';
|
||
import {
|
||
AlertDialog,
|
||
AlertDialogContent,
|
||
AlertDialogDescription,
|
||
AlertDialogFooter,
|
||
AlertDialogHeader,
|
||
AlertDialogTitle,
|
||
} from '@/components/ui/alert-dialog';
|
||
import { useDeleteEntity } from '@/hooks/useDeleteEntity';
|
||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||
import { useToast } from '@/hooks/useToast';
|
||
import { cn } from '@/lib/utils';
|
||
import type { NostrEvent } from '@nostrify/nostrify';
|
||
|
||
interface DeleteEntityDialogProps {
|
||
/** The addressable event to delete (crew, Lab profile, spot, …). */
|
||
event: NostrEvent;
|
||
/** Human label for the thing being deleted, e.g. "crew" or "coach profile". */
|
||
entityLabel: string;
|
||
/** The name/title shown in the confirmation copy. */
|
||
entityName: string;
|
||
/** Where to send the user after a successful delete (e.g. "/crews"). */
|
||
redirectTo?: string;
|
||
/** Verb shown on the trigger button (default "Delete"). */
|
||
triggerText?: string;
|
||
/** Optional custom trigger. If omitted a default outline button is rendered. */
|
||
trigger?: ReactNode;
|
||
/** Extra classes for the default trigger button. */
|
||
className?: string;
|
||
}
|
||
|
||
/**
|
||
* Reusable "delete / dissolve" control for any addressable event the current
|
||
* user owns. Publishes a NIP-09 deletion request (see useDeleteEntity) after an
|
||
* explicit confirmation, and is only rendered for the owner.
|
||
*/
|
||
export function DeleteEntityDialog({
|
||
event,
|
||
entityLabel,
|
||
entityName,
|
||
redirectTo,
|
||
triggerText = 'Delete',
|
||
trigger,
|
||
className,
|
||
}: DeleteEntityDialogProps) {
|
||
const { user } = useCurrentUser();
|
||
const { mutateAsync: deleteEntity, isPending } = useDeleteEntity();
|
||
const { toast } = useToast();
|
||
const navigate = useNavigate();
|
||
const [open, setOpen] = useState(false);
|
||
|
||
// Only the owner can delete their own event.
|
||
if (!user || user.pubkey !== event.pubkey) return null;
|
||
|
||
const handleDelete = async () => {
|
||
try {
|
||
await deleteEntity({ event, reason: `Deleted ${entityLabel} "${entityName}"` });
|
||
toast({
|
||
title: 'Deletion requested',
|
||
description: `Your ${entityLabel} has been removed. Relays that honor deletions will stop serving it.`,
|
||
});
|
||
setOpen(false);
|
||
if (redirectTo) navigate(redirectTo);
|
||
} catch (error) {
|
||
toast({
|
||
title: 'Delete failed',
|
||
description: error instanceof Error ? error.message : 'Something went wrong',
|
||
variant: 'destructive',
|
||
});
|
||
}
|
||
};
|
||
|
||
return (
|
||
<>
|
||
{trigger ? (
|
||
<span onClick={() => setOpen(true)}>{trigger}</span>
|
||
) : (
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => setOpen(true)}
|
||
className={cn(
|
||
'gap-1.5 border-red-500/40 text-red-400 hover:bg-red-500/10 hover:text-red-300',
|
||
className,
|
||
)}
|
||
>
|
||
<Trash2 className="h-3.5 w-3.5" /> {triggerText}
|
||
</Button>
|
||
)}
|
||
|
||
<AlertDialog open={open} onOpenChange={setOpen}>
|
||
<AlertDialogContent>
|
||
<AlertDialogHeader>
|
||
<AlertDialogTitle className="flex items-center gap-2">
|
||
<Trash2 className="h-5 w-5 text-red-500" />
|
||
Delete "{entityName}"?
|
||
</AlertDialogTitle>
|
||
<AlertDialogDescription asChild>
|
||
<div className="space-y-3">
|
||
<p>
|
||
This asks relays to remove your {entityLabel}. This can’t be undone from here — you’d
|
||
have to create a new one.
|
||
</p>
|
||
<div className="flex gap-2 rounded-lg border border-yellow-500/30 bg-yellow-500/10 p-3 text-sm">
|
||
<AlertTriangle className="mt-0.5 h-4 w-4 flex-shrink-0 text-yellow-500" />
|
||
<p className="text-yellow-400/90">
|
||
Nostr is decentralized, so deletion is best-effort: relays that honor NIP-09 will
|
||
drop it, but a relay that doesn’t may keep a copy.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</AlertDialogDescription>
|
||
</AlertDialogHeader>
|
||
<AlertDialogFooter>
|
||
<Button variant="outline" onClick={() => setOpen(false)} disabled={isPending}>
|
||
Cancel
|
||
</Button>
|
||
<Button
|
||
onClick={handleDelete}
|
||
disabled={isPending}
|
||
className="bg-red-500 text-white hover:bg-red-600"
|
||
>
|
||
{isPending ? (
|
||
<>
|
||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||
Deleting…
|
||
</>
|
||
) : (
|
||
<>
|
||
<Trash2 className="mr-2 h-4 w-4" />
|
||
{triggerText}
|
||
</>
|
||
)}
|
||
</Button>
|
||
</AlertDialogFooter>
|
||
</AlertDialogContent>
|
||
</AlertDialog>
|
||
</>
|
||
);
|
||
}
|