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
This commit is contained in:
2025-12-02 01:14:21 +00:00
parent 85c8a1650b
commit 54612a4782
7 changed files with 215 additions and 40 deletions

View File

@@ -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 (
<main className="container mx-auto px-4 sm:px-6 py-6 sm:py-8 max-w-3xl">
{/* Header */}
@@ -36,7 +46,7 @@ export default async function EditItemPage({ params }: PageProps) {
{/* Form */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-5 sm:p-6 lg:p-8">
<ItemForm listId={params.id} item={item} mode="edit" />
<ItemForm listId={params.id} item={item} mode="edit" canEdit={canEdit} />
</div>
</main>
)

View File

@@ -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 (
<main className="container mx-auto px-4 sm:px-6 py-6 sm:py-8 max-w-4xl">
{/* Header Actions */}
@@ -35,21 +54,40 @@ export default async function ItemDetailPage({ params }: PageProps) {
</Link>
<div className="flex items-center gap-2 w-full sm:w-auto">
<ToggleItemButton itemId={item.id} completed={item.completed} />
<Link
href={`/list/${params.id}/item/${params.itemId}/edit`}
className="flex items-center gap-2 px-4 sm:px-5 py-2.5 sm:py-3 bg-blue-500 hover:bg-blue-600 active:bg-blue-700 text-white rounded-lg transition-colors tap-target text-sm sm:text-base font-medium"
>
<Edit size={18} className="flex-shrink-0" />
<span>Edit</span>
</Link>
<DeleteItemButton itemId={item.id} listId={params.id} />
{canEdit && (
<>
<ToggleItemButton itemId={item.id} completed={item.completed} />
<Link
href={`/list/${params.id}/item/${params.itemId}/edit`}
className="flex items-center gap-2 px-4 sm:px-5 py-2.5 sm:py-3 bg-blue-500 hover:bg-blue-600 active:bg-blue-700 text-white rounded-lg transition-colors tap-target text-sm sm:text-base font-medium"
>
<Edit size={18} className="flex-shrink-0" />
<span>Edit</span>
</Link>
<DeleteItemButton itemId={item.id} listId={params.id} />
</>
)}
</div>
</div>
{/* Item Detail */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-5 sm:p-6 lg:p-8">
<ItemDetail item={item} listTitle={item.list.title} />
<ItemDetail
item={{
id: item.id,
title: item.title,
description: item.description,
completed: item.completed,
priority: item.priority,
dueDate: item.dueDate,
tags: item.tags,
notes: item.notes,
createdAt: item.createdAt,
updatedAt: item.updatedAt,
listId: item.listId,
}}
listTitle={item.list.title}
/>
</div>
</main>
)

View File

@@ -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
</p>
</div>
{/* Inline Item Creation */}
<div className="mb-6 sm:mb-8">
<InlineItemCreate listId={list.id} />
</div>
{/* Inline Item Creation - only show if user can edit */}
{canEdit && (
<div className="mb-6 sm:mb-8">
<InlineItemCreate listId={list.id} />
</div>
)}
{/* Search, Filter, and Items List */}
<ListDetailView listId={list.id} initialItems={list.items} />
<ListDetailView
listId={list.id}
initialItems={list.items}
permission={permission}
canShare={canShare}
listTitle={list.title}
ownerId={list.ownerId}
ownerName={list.owner?.name}
ownerEmail={list.owner?.email || ''}
collaborators={list.collaborators.map(c => ({
...c,
permission: c.permission as 'editor' | 'viewer'
}))}
/>
</main>
)
}