From 5f0934acc0f7dde119be9f0a42a42742ec128377 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sun, 23 Nov 2025 00:54:38 +0000 Subject: feat: Add invitation approval for shared lists (#2152) * feat: Add invitation approval system for collaborative lists - Add database schema changes to support pending invitations - Add status field (pending/accepted/declined) to listCollaborators - Add invitedAt and invitedEmail fields for tracking - Add index on status for efficient queries - Update List model with invitation workflow methods - Modify addCollaboratorByEmail to create pending invitations - Add acceptInvitation() for users to accept invites - Add declineInvitation() for users to decline invites - Add revokeInvitation() for owners to revoke pending invites - Add getPendingInvitations() to get user's pending invites - Implement privacy protection for pending invitations - Mask user names as "Pending User" until invitation is accepted - Only show email to list owner for pending invitations - Update getSharedWithUser to only include accepted collaborations - Ensures lists only appear after invitation is accepted * feat: Add tRPC procedures and email notifications for list invitations - Add new tRPC procedures for invitation workflow - acceptInvitation: Allow users to accept pending invitations - declineInvitation: Allow users to decline invitations - revokeInvitation: Allow owners to revoke pending invitations - getPendingInvitations: Get all pending invitations for current user - Update getCollaborators output schema - Add status, invitedAt fields to collaborator objects - Support privacy-masked user info for pending invitations - Add sendListInvitationEmail function - Email notification when user is invited to collaborate - Includes list name, inviter name, and link to view invitation - Gracefully handles missing SMTP configuration - Integrate email sending into invitation workflow - Send email when new invitation is created - Send email when declined invitation is renewed - Catch and log errors without failing the invitation * feat: Add UI for list invitation approval workflow - Update ManageCollaboratorsModal to support pending invitations - Show "Pending" badge for pending invitations - Add revoke button for owners to cancel pending invitations - Update success message to reflect invitation sent - Disable role change and remove buttons for pending invitations - Create PendingInvitationsCard component - Display all pending invitations for the current user - Show list name, description, inviter, and role - Provide Accept and Decline buttons - Auto-hide when no pending invitations exist - Add PendingInvitationsCard to lists page - Show at the top of the lists page - Only renders when user has pending invitations * fix: Add missing translation keys and fix TypeScript errors - Add translation keys for invitation system - lists.collaborators.invitation_sent - lists.collaborators.pending - lists.collaborators.revoke - lists.collaborators.invitation_revoked - lists.collaborators.failed_to_revoke - lists.invitations.* (all invitation-related keys) - Fix TypeScript errors in email sending - Handle optional user.name with fallback to 'A user' * wip * fixes * more fixes * fix revoke * more improvements * comment fix * fix email url * fix schemas * split pending invites into components * more fixes * test * test fixes --------- Co-authored-by: Claude --- .../dashboard/lists/PendingInvitationsCard.tsx | 158 +++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 apps/web/components/dashboard/lists/PendingInvitationsCard.tsx (limited to 'apps/web/components/dashboard/lists/PendingInvitationsCard.tsx') diff --git a/apps/web/components/dashboard/lists/PendingInvitationsCard.tsx b/apps/web/components/dashboard/lists/PendingInvitationsCard.tsx new file mode 100644 index 00000000..c453a91f --- /dev/null +++ b/apps/web/components/dashboard/lists/PendingInvitationsCard.tsx @@ -0,0 +1,158 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { toast } from "@/components/ui/use-toast"; +import { useTranslation } from "@/lib/i18n/client"; +import { api } from "@/lib/trpc"; +import { Check, Loader2, Mail, X } from "lucide-react"; + +interface Invitation { + id: string; + role: string; + list: { + name: string; + icon?: string; + description?: string | null; + owner?: { + name?: string; + } | null; + }; +} + +function InvitationRow({ invitation }: { invitation: Invitation }) { + const { t } = useTranslation(); + const utils = api.useUtils(); + + const acceptInvitation = api.lists.acceptInvitation.useMutation({ + onSuccess: async () => { + toast({ + description: t("lists.invitations.accepted"), + }); + await Promise.all([ + utils.lists.getPendingInvitations.invalidate(), + utils.lists.list.invalidate(), + ]); + }, + onError: (error) => { + toast({ + variant: "destructive", + description: error.message || t("lists.invitations.failed_to_accept"), + }); + }, + }); + + const declineInvitation = api.lists.declineInvitation.useMutation({ + onSuccess: async () => { + toast({ + description: t("lists.invitations.declined"), + }); + await utils.lists.getPendingInvitations.invalidate(); + }, + onError: (error) => { + toast({ + variant: "destructive", + description: error.message || t("lists.invitations.failed_to_decline"), + }); + }, + }); + + return ( +
+
+
+ {invitation.list.name} + + {invitation.list.icon} + +
+ {invitation.list.description && ( +
+ {invitation.list.description} +
+ )} +
+ {t("lists.invitations.invited_by")}{" "} + + {invitation.list.owner?.name || "Unknown"} + + {" • "} + {invitation.role} +
+
+
+ + +
+
+ ); +} + +export function PendingInvitationsCard() { + const { t } = useTranslation(); + + const { data: invitations, isLoading } = + api.lists.getPendingInvitations.useQuery(); + + if (isLoading) { + return null; + } + + if (!invitations || invitations.length === 0) { + return null; + } + + return ( + + + + + {t("lists.invitations.pending")} ({invitations.length}) + + {t("lists.invitations.description")} + + + {invitations.map((invitation) => ( + + ))} + + + ); +} -- cgit v1.2.3-70-g09d2