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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<form onSubmit={handleSubmit} className="space-y-5 sm:space-y-6">
|
||||
{!canEdit && (
|
||||
<div className="p-3 sm:p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<p className="text-xs sm:text-sm text-yellow-800">
|
||||
You have view-only access to this list. You cannot edit items.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Title */}
|
||||
<div>
|
||||
<label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
@@ -249,11 +263,12 @@ export default function ItemForm({ listId, item, mode = 'edit' }: ItemFormProps)
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
onBlur={() => handleBlur('title')}
|
||||
disabled={!canEdit}
|
||||
className={`w-full px-3 sm:px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 text-sm sm:text-base ${
|
||||
errors.title && touched.title
|
||||
? 'border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:ring-blue-500'
|
||||
}`}
|
||||
} ${!canEdit ? 'bg-gray-100 cursor-not-allowed' : ''}`}
|
||||
placeholder="Enter task title"
|
||||
maxLength={200}
|
||||
aria-invalid={errors.title && touched.title ? 'true' : 'false'}
|
||||
@@ -276,12 +291,13 @@ export default function ItemForm({ listId, item, mode = 'edit' }: ItemFormProps)
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
onBlur={() => handleBlur('description')}
|
||||
disabled={!canEdit}
|
||||
rows={3}
|
||||
className={`w-full px-3 sm:px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 text-sm sm:text-base resize-y ${
|
||||
errors.description && touched.description
|
||||
? 'border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:ring-blue-500'
|
||||
}`}
|
||||
} ${!canEdit ? 'bg-gray-100 cursor-not-allowed' : ''}`}
|
||||
placeholder="Add a description"
|
||||
maxLength={2000}
|
||||
aria-invalid={errors.description && touched.description ? 'true' : 'false'}
|
||||
@@ -303,7 +319,8 @@ export default function ItemForm({ listId, item, mode = 'edit' }: ItemFormProps)
|
||||
id="priority"
|
||||
value={priority}
|
||||
onChange={(e) => setPriority(e.target.value as 'low' | 'medium' | 'high')}
|
||||
className="w-full px-3 sm:px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm sm:text-base tap-target"
|
||||
disabled={!canEdit}
|
||||
className={`w-full px-3 sm:px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm sm:text-base tap-target ${!canEdit ? 'bg-gray-100 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
<option value="low">Low</option>
|
||||
<option value="medium">Medium</option>
|
||||
@@ -322,11 +339,12 @@ export default function ItemForm({ listId, item, mode = 'edit' }: ItemFormProps)
|
||||
value={dueDate}
|
||||
onChange={(e) => setDueDate(e.target.value)}
|
||||
onBlur={() => handleBlur('dueDate')}
|
||||
disabled={!canEdit}
|
||||
className={`w-full px-3 sm:px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 text-sm sm:text-base tap-target ${
|
||||
errors.dueDate && touched.dueDate
|
||||
? 'border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:ring-blue-500'
|
||||
}`}
|
||||
} ${!canEdit ? 'bg-gray-100 cursor-not-allowed' : ''}`}
|
||||
min="1900-01-01"
|
||||
max="2100-12-31"
|
||||
aria-invalid={errors.dueDate && touched.dueDate ? 'true' : 'false'}
|
||||
@@ -351,11 +369,12 @@ export default function ItemForm({ listId, item, mode = 'edit' }: ItemFormProps)
|
||||
value={tagInput}
|
||||
onChange={(e) => setTagInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={!canEdit}
|
||||
className={`flex-1 px-3 sm:px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 text-sm sm:text-base ${
|
||||
errors.tagInput
|
||||
? 'border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:ring-blue-500'
|
||||
}`}
|
||||
} ${!canEdit ? 'bg-gray-100 cursor-not-allowed' : ''}`}
|
||||
placeholder="Add a tag"
|
||||
maxLength={50}
|
||||
aria-invalid={errors.tagInput ? 'true' : 'false'}
|
||||
@@ -364,7 +383,7 @@ export default function ItemForm({ listId, item, mode = 'edit' }: ItemFormProps)
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddTag}
|
||||
disabled={tags.length >= 20}
|
||||
disabled={tags.length >= 20 || !canEdit}
|
||||
className="px-4 sm:px-5 py-3 bg-gray-200 hover:bg-gray-300 active:bg-gray-400 disabled:bg-gray-100 disabled:text-gray-400 text-gray-700 rounded-lg transition-colors tap-target text-sm sm:text-base font-medium"
|
||||
>
|
||||
Add
|
||||
@@ -389,7 +408,8 @@ export default function ItemForm({ listId, item, mode = 'edit' }: ItemFormProps)
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveTag(tag)}
|
||||
className="hover:text-blue-600 active:text-blue-700 tap-target-sm text-lg leading-none"
|
||||
disabled={!canEdit}
|
||||
className="hover:text-blue-600 active:text-blue-700 tap-target-sm text-lg leading-none disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
aria-label={`Remove ${tag} tag`}
|
||||
>
|
||||
×
|
||||
@@ -410,12 +430,13 @@ export default function ItemForm({ listId, item, mode = 'edit' }: ItemFormProps)
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
onBlur={() => handleBlur('notes')}
|
||||
disabled={!canEdit}
|
||||
rows={5}
|
||||
className={`w-full px-3 sm:px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 text-sm sm:text-base resize-y ${
|
||||
errors.notes && touched.notes
|
||||
? 'border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:ring-blue-500'
|
||||
}`}
|
||||
} ${!canEdit ? 'bg-gray-100 cursor-not-allowed' : ''}`}
|
||||
placeholder="Add additional notes"
|
||||
maxLength={5000}
|
||||
aria-invalid={errors.notes && touched.notes ? 'true' : 'false'}
|
||||
@@ -442,15 +463,17 @@ export default function ItemForm({ listId, item, mode = 'edit' }: ItemFormProps)
|
||||
onClick={() => router.back()}
|
||||
className="w-full sm:w-auto px-4 sm:px-5 py-3 bg-gray-200 hover:bg-gray-300 active:bg-gray-400 text-gray-700 rounded-lg transition-colors tap-target font-medium text-sm sm:text-base"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="w-full sm:flex-1 bg-blue-500 hover:bg-blue-600 active:bg-blue-700 disabled:bg-blue-300 text-white px-4 sm:px-5 py-3 rounded-lg transition-colors tap-target font-medium text-sm sm:text-base"
|
||||
>
|
||||
{isSubmitting ? 'Saving...' : mode === 'create' ? 'Create Task' : 'Save Changes'}
|
||||
{canEdit ? 'Cancel' : 'Back'}
|
||||
</button>
|
||||
{canEdit && (
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="w-full sm:flex-1 bg-blue-500 hover:bg-blue-600 active:bg-blue-700 disabled:bg-blue-300 text-white px-4 sm:px-5 py-3 rounded-lg transition-colors tap-target font-medium text-sm sm:text-base"
|
||||
>
|
||||
{isSubmitting ? 'Saving...' : mode === 'create' ? 'Create Task' : 'Save Changes'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
|
||||
@@ -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 }}
|
||||
/>
|
||||
<h3 className="font-semibold text-gray-900 truncate text-base sm:text-lg">{list.title}</h3>
|
||||
{hasCollaborators && list.collaborators && (
|
||||
<div className="flex items-center gap-1 text-gray-500 flex-shrink-0" title={`Shared with ${list.collaborators.length} ${list.collaborators.length === 1 ? 'person' : 'people'}`}>
|
||||
<Users size={16} />
|
||||
<span className="text-xs">{list.collaborators.length}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5 sm:gap-1 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity flex-shrink-0" onClick={handleDeleteClick}>
|
||||
<button
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useCallback, useMemo, useEffect } from 'react'
|
||||
import { Share2 } from 'lucide-react'
|
||||
import ItemCard from './ItemCard'
|
||||
import SearchBar from './SearchBar'
|
||||
import FilterPanel from './FilterPanel'
|
||||
import SearchResults from './SearchResults'
|
||||
import EmptyState from './EmptyState'
|
||||
import ShareListModal from './ShareListModal'
|
||||
import PermissionBadge from './PermissionBadge'
|
||||
import { searchItems, getItemsByFilter } from '@/lib/actions'
|
||||
import { useSearch, useFilters, useScrollPosition } from '@/hooks'
|
||||
|
||||
@@ -34,12 +37,38 @@ interface ItemWithList extends Item {
|
||||
interface ListDetailViewProps {
|
||||
listId: string
|
||||
initialItems: Item[]
|
||||
permission?: 'owner' | 'editor' | 'viewer'
|
||||
canShare?: boolean
|
||||
listTitle?: string
|
||||
ownerId?: string
|
||||
ownerName?: string | null
|
||||
ownerEmail?: string
|
||||
collaborators?: Array<{
|
||||
id: string
|
||||
permission: 'editor' | 'viewer'
|
||||
user: {
|
||||
id: string
|
||||
name: string | null
|
||||
email: string
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
export default function ListDetailView({ listId, initialItems }: ListDetailViewProps) {
|
||||
export default function ListDetailView({
|
||||
listId,
|
||||
initialItems,
|
||||
permission,
|
||||
canShare = false,
|
||||
listTitle = '',
|
||||
ownerId = '',
|
||||
ownerName = null,
|
||||
ownerEmail = '',
|
||||
collaborators = []
|
||||
}: ListDetailViewProps) {
|
||||
const [searchResults, setSearchResults] = useState<ItemWithList[]>([])
|
||||
const [isSearching, setIsSearching] = useState(false)
|
||||
const [filteredItems, setFilteredItems] = useState<Item[]>(initialItems)
|
||||
const [isShareModalOpen, setIsShareModalOpen] = useState(false)
|
||||
|
||||
// Use scroll position preservation hook
|
||||
useScrollPosition({ key: `list-${listId}` })
|
||||
@@ -118,6 +147,26 @@ export default function ListDetailView({ listId, initialItems }: ListDetailViewP
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Permission Badge and Share Button */}
|
||||
{(permission || canShare) && (
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
{permission && (
|
||||
<div>
|
||||
<PermissionBadge permission={permission} />
|
||||
</div>
|
||||
)}
|
||||
{canShare && (
|
||||
<button
|
||||
onClick={() => setIsShareModalOpen(true)}
|
||||
className="ml-auto flex items-center gap-2 px-4 py-2 bg-blue-500 hover:bg-blue-600 active:bg-blue-700 text-white rounded-lg transition-colors text-sm font-medium"
|
||||
>
|
||||
<Share2 size={16} />
|
||||
Share
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search Bar */}
|
||||
<SearchBar
|
||||
onSearch={handleSearchCallback}
|
||||
@@ -181,6 +230,19 @@ export default function ListDetailView({ listId, initialItems }: ListDetailViewP
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Share Modal */}
|
||||
<ShareListModal
|
||||
isOpen={isShareModalOpen}
|
||||
onClose={() => setIsShareModalOpen(false)}
|
||||
listId={listId}
|
||||
listTitle={listTitle}
|
||||
isOwner={permission === 'owner'}
|
||||
ownerId={ownerId}
|
||||
ownerName={ownerName}
|
||||
ownerEmail={ownerEmail}
|
||||
initialCollaborators={collaborators}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user