Files
Todo/components/ListDetailView.tsx
Paul Bailey b2a59eecf1 feat: implement real-time list synchronization
- Add useRealtimeSync hook for polling server updates every 3 seconds
- Create API endpoint to check last updated timestamp for lists
- Integrate real-time sync into ListDetailView component
- Automatically refresh when other users make changes to shared lists
- Poll on interval and when page becomes visible again
2025-12-02 10:06:49 +00:00

257 lines
7.2 KiB
TypeScript

'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'
import { useRealtimeSync } from '@/hooks/useRealtimeSync'
interface Item {
id: string
title: string
description: string | null
completed: boolean
priority: string
dueDate: Date | null
tags: any
notes: string | null
createdAt: Date
updatedAt: Date
listId: string
}
interface ItemWithList extends Item {
list: {
id: string
title: string
color: string
}
}
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,
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}` })
// Use real-time sync hook to poll for updates
useRealtimeSync({
listId,
enabled: true,
pollInterval: 3000, // Poll every 3 seconds
})
// Extract all unique tags from items
const availableTags = useMemo(() => {
const tagSet = new Set<string>()
initialItems.forEach(item => {
const tags = Array.isArray(item.tags) ? item.tags : []
tags.forEach(tag => {
if (typeof tag === 'string') {
tagSet.add(tag)
}
})
})
return Array.from(tagSet).sort()
}, [initialItems])
// Use search hook
const handleSearchCallback = useCallback(async (query: string) => {
if (!query.trim()) {
setSearchResults([])
setIsSearching(false)
return
}
setIsSearching(true)
try {
const results = await searchItems(query, listId)
setSearchResults(results as ItemWithList[])
} catch (error) {
console.error('Search error:', error)
setSearchResults([])
} finally {
setIsSearching(false)
}
}, [listId])
const { debouncedQuery } = useSearch({
onSearch: handleSearchCallback
})
// Use filters hook
const handleFiltersCallback = useCallback(async (newFilters: any) => {
try {
const results = await getItemsByFilter(listId, {
priority: newFilters.priority,
completed: newFilters.completed,
tags: newFilters.tags,
dueDateRange: newFilters.dueDateRange,
sortBy: newFilters.sortBy,
sortOrder: newFilters.sortOrder
})
// Extract items from pagination result
const items = 'items' in results ? results.items : results
setFilteredItems(items as Item[])
} catch (error) {
console.error('Filter error:', error)
setFilteredItems(initialItems)
}
}, [listId, initialItems])
const { filters, setFilters } = useFilters({
onFiltersChange: handleFiltersCallback
})
// Apply initial filters on mount
useEffect(() => {
handleFiltersCallback(filters)
}, []) // Only run on mount
// Determine which items to display
const displayItems = debouncedQuery.trim() ? searchResults : filteredItems
const completedItems = displayItems.filter(item => item.completed)
const incompleteItems = displayItems.filter(item => !item.completed)
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}
placeholder="Search tasks in this list..."
/>
{/* Filter Panel */}
<FilterPanel
filters={filters}
onFiltersChange={setFilters}
availableTags={availableTags}
/>
{/* Results */}
{debouncedQuery.trim() ? (
<SearchResults
results={searchResults}
query={debouncedQuery}
/>
) : (
<div className="space-y-6">
{/* Incomplete Items */}
{incompleteItems.length > 0 && (
<div className="animate-fade-in">
<h2 className="text-lg font-semibold text-gray-900 mb-3">
To Do ({incompleteItems.length})
</h2>
<div className="space-y-2">
{incompleteItems.map((item) => (
<ItemCard key={item.id} item={item} listId={listId} />
))}
</div>
</div>
)}
{/* Completed Items */}
{completedItems.length > 0 && (
<div className="animate-fade-in">
<h2 className="text-lg font-semibold text-gray-500 mb-3">
Completed ({completedItems.length})
</h2>
<div className="space-y-2">
{completedItems.map((item) => (
<ItemCard key={item.id} item={item} listId={listId} />
))}
</div>
</div>
)}
{/* Empty State */}
{displayItems.length === 0 && (
<EmptyState
icon={filters.priority || filters.completed !== null || filters.tags.length > 0 ? 'filter' : 'inbox'}
title="No tasks found"
description={
filters.priority || filters.completed !== null || filters.tags.length > 0
? 'Try adjusting your filters to see more tasks'
: 'Add your first task using the button above'
}
/>
)}
</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>
)
}