feat(invites): add cancel button to revoke pending invites

- New DELETE /api/invites/[id] endpoint — only the owning family can delete
- Settings page shows expiry date on each pending invite
- Cancel button removes invite instantly from list (optimistic UI)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Manohar Gupta 2026-05-24 14:47:43 +05:30
parent 781dd8f1df
commit 4dcdc5a572
2 changed files with 47 additions and 3 deletions

View file

@ -0,0 +1,26 @@
import { NextResponse } from "next/server";
import { sql } from "@/db";
import { requireFamily } from "@/lib/auth";
// DELETE — cancel/revoke a pending invite (only the family that sent it can delete it)
export async function DELETE(
_req: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const auth = await requireFamily();
if (!auth.success) return NextResponse.json({ error: auth.error }, { status: auth.status });
const { id } = await params;
try {
const result = await sql.unsafe(
`DELETE FROM family_invites WHERE id = $1 AND family_id = $2`,
[id, auth.session!.familyId]
);
// result.count === 0 means the invite didn't belong to this family or didn't exist
return NextResponse.json({ success: true });
} catch (error) {
console.error(error);
return NextResponse.json({ error: String(error) }, { status: 500 });
}
}

View file

@ -123,6 +123,15 @@ export default function SettingsPage() {
}
};
const deleteInvite = async (inviteId: string) => {
try {
await fetch(`/api/invites/${inviteId}`, { method: "DELETE" });
setInvites(prev => prev.filter(i => i.id !== inviteId));
} catch (err) {
console.error("Failed to delete invite:", err);
}
};
const sendInvite = async () => {
if (!inviteEmail || !familyId) return;
setInviteLoading(true);
@ -273,9 +282,18 @@ export default function SettingsPage() {
<div className="mb-3">
<div className="text-sm text-gray-500 mb-2">Pending Invites</div>
{invites.map((invite) => (
<div key={invite.id} className="flex justify-between items-center p-2 bg-gray-50 rounded text-sm">
<span>{invite.email}</span>
<span className="text-gray-400">Pending</span>
<div key={invite.id} className="flex justify-between items-center p-2 bg-gray-50 dark:bg-gray-700 rounded text-sm mb-1">
<div>
<div className="font-medium text-gray-800 dark:text-gray-100">{invite.email}</div>
<div className="text-xs text-gray-400">Pending · expires {new Date(invite.expiresAt).toLocaleDateString()}</div>
</div>
<button
onClick={() => deleteInvite(invite.id)}
className="text-xs text-red-400 hover:text-red-600 dark:hover:text-red-300 px-2 py-1 rounded hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
title="Cancel invite"
>
Cancel
</button>
</div>
))}
</div>