From 5f0934acc0f7dde119be9f0a42a42742ec128377 Mon Sep 17 00:00:00 2001
From: Mohamed Bassem <me@mbassem.com>
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 <noreply@anthropic.com>
---
 apps/web/app/dashboard/lists/page.tsx | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

(limited to 'apps/web/app/dashboard/lists/page.tsx')

diff --git a/apps/web/app/dashboard/lists/page.tsx b/apps/web/app/dashboard/lists/page.tsx
index 1c28073d..7950cd76 100644
--- a/apps/web/app/dashboard/lists/page.tsx
+++ b/apps/web/app/dashboard/lists/page.tsx
@@ -1,4 +1,5 @@
 import AllListsView from "@/components/dashboard/lists/AllListsView";
+import { PendingInvitationsCard } from "@/components/dashboard/lists/PendingInvitationsCard";
 import { Separator } from "@/components/ui/separator";
 import { useTranslation } from "@/lib/i18n/server";
 import { api } from "@/server/api/client";
@@ -9,10 +10,13 @@ export default async function ListsPage() {
   const lists = await api.lists.list();
 
   return (
-    <div className="flex flex-col gap-3 rounded-md border bg-background p-4">
-      <p className="text-2xl">📋 {t("lists.all_lists")}</p>
-      <Separator />
-      <AllListsView initialData={lists.lists} />
+    <div className="flex flex-col gap-4">
+      <PendingInvitationsCard />
+      <div className="flex flex-col gap-3 rounded-md border bg-background p-4">
+        <p className="text-2xl">📋 {t("lists.all_lists")}</p>
+        <Separator />
+        <AllListsView initialData={lists.lists} />
+      </div>
     </div>
   );
 }
-- 
cgit v1.2.3-70-g09d2

