From 54612a478241f9af65b68ecf311fb7ad7a0e0dfb Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Tue, 2 Dec 2025 01:14:21 +0000 Subject: [PATCH] feat: update components for multi-user support - Add sharing indicator to ListCard showing collaborator count - Add permission badge and share button to ListDetailView - Disable editing in ItemForm for viewers with visual feedback - Add permission checks to item detail and edit pages - Hide edit/delete buttons for viewers on item detail page - Pass permission and collaborator data through component hierarchy - Update navigation to include profile and invitations links (already done in previous tasks) Implements task 15 from user-auth-and-sharing spec --- .kiro/specs/user-auth-and-sharing/tasks.md | 4 +- app/list/[id]/item/[itemId]/edit/page.tsx | 12 +++- app/list/[id]/item/[itemId]/page.tsx | 64 +++++++++++++++++----- app/list/[id]/page.tsx | 40 ++++++++++++-- components/ItemForm.tsx | 57 +++++++++++++------ components/ListCard.tsx | 14 ++++- components/ListDetailView.tsx | 64 +++++++++++++++++++++- 7 files changed, 215 insertions(+), 40 deletions(-) diff --git a/.kiro/specs/user-auth-and-sharing/tasks.md b/.kiro/specs/user-auth-and-sharing/tasks.md index e30d9d4..a978f91 100644 --- a/.kiro/specs/user-auth-and-sharing/tasks.md +++ b/.kiro/specs/user-auth-and-sharing/tasks.md @@ -208,7 +208,7 @@ - Add invitations page - _Requirements: 5.1, 5.4, 8.1, 8.2_ -- [-] 14. Implement profile management +- [x] 14. Implement profile management - Create profile page and ProfileForm component - Implement updateProfile server action - Create PasswordChangeForm component @@ -236,7 +236,7 @@ - **Property 29: Profile data exposure protection** - **Validates: Requirements 7.5** -- [ ] 15. Update existing components for multi-user +- [x] 15. Update existing components for multi-user - Modify ListCard to show sharing indicator - Update ListDetailView with share button and permission display - Modify ItemForm to disable editing for viewers diff --git a/app/list/[id]/item/[itemId]/edit/page.tsx b/app/list/[id]/item/[itemId]/edit/page.tsx index ad4a7fe..10cb47c 100644 --- a/app/list/[id]/item/[itemId]/edit/page.tsx +++ b/app/list/[id]/item/[itemId]/edit/page.tsx @@ -1,5 +1,7 @@ import { notFound } from 'next/navigation' import { getItem } from '@/lib/actions' +import { canEditList } from '@/lib/permissions' +import { auth } from '@/auth' import ItemForm from '@/components/ItemForm' import Link from 'next/link' import { ArrowLeft } from 'lucide-react' @@ -14,12 +16,20 @@ interface PageProps { } export default async function EditItemPage({ params }: PageProps) { + const session = await auth() + if (!session?.user?.id) { + notFound() + } + const item = await getItem(params.itemId) if (!item || item.listId !== params.id) { notFound() } + // Check if user can edit this list + const canEdit = await canEditList(session.user.id, params.id) + return (
{/* Header */} @@ -36,7 +46,7 @@ export default async function EditItemPage({ params }: PageProps) { {/* Form */}
- +
) diff --git a/app/list/[id]/item/[itemId]/page.tsx b/app/list/[id]/item/[itemId]/page.tsx index 498eb55..410759d 100644 --- a/app/list/[id]/item/[itemId]/page.tsx +++ b/app/list/[id]/item/[itemId]/page.tsx @@ -1,5 +1,7 @@ import { notFound } from 'next/navigation' import { getItem } from '@/lib/actions' +import { canEditList } from '@/lib/permissions' +import { auth } from '@/auth' import ItemDetail from '@/components/ItemDetail' import Link from 'next/link' import { ArrowLeft, Edit } from 'lucide-react' @@ -16,12 +18,29 @@ interface PageProps { } export default async function ItemDetailPage({ params }: PageProps) { - const item = await getItem(params.itemId) - - if (!item || item.listId !== params.id) { + const session = await auth() + if (!session?.user?.id) { notFound() } + const itemData = await getItem(params.itemId) + + if (!itemData || itemData.listId !== params.id) { + notFound() + } + + // Type assertion for the item with list relation + const item = itemData as typeof itemData & { + list: { + title: string + color: string + ownerId: string + } + } + + // Check if user can edit this list + const canEdit = await canEditList(session.user.id, params.id) + return (
{/* Header Actions */} @@ -35,21 +54,40 @@ export default async function ItemDetailPage({ params }: PageProps) {
- - - - Edit - - + {canEdit && ( + <> + + + + Edit + + + + )}
{/* Item Detail */}
- +
) diff --git a/app/list/[id]/page.tsx b/app/list/[id]/page.tsx index 0637101..3c9dd09 100644 --- a/app/list/[id]/page.tsx +++ b/app/list/[id]/page.tsx @@ -1,4 +1,6 @@ import { getListWithItems } from '@/lib/actions' +import { getPermission } from '@/lib/permissions' +import { auth } from '@/auth' import { notFound } from 'next/navigation' import Link from 'next/link' import { ArrowLeft } from 'lucide-react' @@ -8,12 +10,25 @@ import ListDetailView from '@/components/ListDetailView' export const dynamic = 'force-dynamic' export default async function ListDetailPage({ params }: { params: { id: string } }) { + const session = await auth() + if (!session?.user?.id) { + notFound() + } + const list = await getListWithItems(params.id) if (!list) { notFound() } + // Get user's permission for this list + const permission = await getPermission(session.user.id, params.id) + if (!permission) { + notFound() + } + + const canShare = permission === 'owner' + const canEdit = permission === 'owner' || permission === 'editor' const completedItems = list.items.filter(item => item.completed) return ( @@ -41,13 +56,28 @@ export default async function ListDetailPage({ params }: { params: { id: string

- {/* Inline Item Creation */} -
- -
+ {/* Inline Item Creation - only show if user can edit */} + {canEdit && ( +
+ +
+ )} {/* Search, Filter, and Items List */} - + ({ + ...c, + permission: c.permission as 'editor' | 'viewer' + }))} + /> ) } diff --git a/components/ItemForm.tsx b/components/ItemForm.tsx index 1fbaf32..2a58988 100644 --- a/components/ItemForm.tsx +++ b/components/ItemForm.tsx @@ -26,9 +26,10 @@ interface ItemFormProps { notes: string | null } mode?: 'create' | 'edit' + canEdit?: boolean } -export default function ItemForm({ listId, item, mode = 'edit' }: ItemFormProps) { +export default function ItemForm({ listId, item, mode = 'edit', canEdit = true }: ItemFormProps) { const router = useRouter() const { showToast } = useToast() const [isSubmitting, setIsSubmitting] = useState(false) @@ -157,6 +158,11 @@ export default function ItemForm({ listId, item, mode = 'edit' }: ItemFormProps) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() + if (!canEdit) { + showToast('You do not have permission to edit this item', 'error') + return + } + if (!validateForm()) { showToast('Please fix validation errors', 'error') return @@ -238,6 +244,14 @@ export default function ItemForm({ listId, item, mode = 'edit' }: ItemFormProps) return (
+ {!canEdit && ( +
+

+ You have view-only access to this list. You cannot edit items. +

+
+ )} + {/* Title */}
) diff --git a/components/ListCard.tsx b/components/ListCard.tsx index b3cb0dc..ad7a2d3 100644 --- a/components/ListCard.tsx +++ b/components/ListCard.tsx @@ -2,7 +2,7 @@ import { memo, useState } from 'react' import Link from 'next/link' -import { Edit2 } from 'lucide-react' +import { Edit2, Users } from 'lucide-react' import Modal from './Modal' import ListForm from './ListForm' import DeleteListButton from './DeleteListButton' @@ -15,6 +15,11 @@ interface List { id: string completed: boolean }> + collaborators?: Array<{ + id: string + userId: string + permission: string + }> } function ListCard({ list }: { list: List }) { @@ -22,6 +27,7 @@ function ListCard({ list }: { list: List }) { const completedCount = list.items.filter(item => item.completed).length const totalCount = list.items.length const progress = totalCount > 0 ? (completedCount / totalCount) * 100 : 0 + const hasCollaborators = list.collaborators && list.collaborators.length > 0 const handleEditClick = (e: React.MouseEvent) => { e.preventDefault() @@ -45,6 +51,12 @@ function ListCard({ list }: { list: List }) { style={{ backgroundColor: list.color }} />

{list.title}

+ {hasCollaborators && list.collaborators && ( +
+ + {list.collaborators.length} +
+ )}
+ )} +
+ )} + {/* Search Bar */} )} + + {/* Share Modal */} + setIsShareModalOpen(false)} + listId={listId} + listTitle={listTitle} + isOwner={permission === 'owner'} + ownerId={ownerId} + ownerName={ownerName} + ownerEmail={ownerEmail} + initialCollaborators={collaborators} + /> ) }