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:
parent
781dd8f1df
commit
4dcdc5a572
2 changed files with 47 additions and 3 deletions
26
src/app/api/invites/[id]/route.ts
Normal file
26
src/app/api/invites/[id]/route.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 () => {
|
const sendInvite = async () => {
|
||||||
if (!inviteEmail || !familyId) return;
|
if (!inviteEmail || !familyId) return;
|
||||||
setInviteLoading(true);
|
setInviteLoading(true);
|
||||||
|
|
@ -273,9 +282,18 @@ export default function SettingsPage() {
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<div className="text-sm text-gray-500 mb-2">Pending Invites</div>
|
<div className="text-sm text-gray-500 mb-2">Pending Invites</div>
|
||||||
{invites.map((invite) => (
|
{invites.map((invite) => (
|
||||||
<div key={invite.id} className="flex justify-between items-center p-2 bg-gray-50 rounded text-sm">
|
<div key={invite.id} className="flex justify-between items-center p-2 bg-gray-50 dark:bg-gray-700 rounded text-sm mb-1">
|
||||||
<span>{invite.email}</span>
|
<div>
|
||||||
<span className="text-gray-400">Pending</span>
|
<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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue