import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Plus, Trash2, Users, Check, Radio, Sparkles, X, Hash, } from 'lucide-react'; import { ListItem, SharedList, ListShare, Team, User, WSMessage } from '../../lib/types'; import { api, buildWebSocketURL, triggerHaptic } from '../../lib/api'; import { UnifiedShareModal } from '../../components/UnifiedShareModal'; import { useMobileActions } from '../../context/MobileActionContext'; interface SharedListsViewProps { onRefreshGlobal: () => void; currentUser?: User | null; } export function parseListItemInput(rawText: string, defaultQty?: string): { content: string; quantity: string; category?: string; } { let text = rawText.trim(); const hashtagRegex = /#([a-zA-Z0-9äöüÄÖÜß_-]+)/g; const matches = Array.from(text.matchAll(hashtagRegex)); let category: string | undefined = undefined; if (matches.length > 0) { category = matches[0][1]; text = text.replace(hashtagRegex, '').replace(/\s+/g, ' ').trim(); } if (!text && matches.length > 0) { text = matches[0][1]; } return { content: text, quantity: defaultQty?.trim() || '', category, }; } export function getItemCategory(item: ListItem): string { if (item.category && item.category.trim()) { return item.category.trim().replace(/^#/, ''); } const match = item.content.match(/#([a-zA-Z0-9äöüÄÖÜß_-]+)/); if (match) { return match[1]; } return ''; } export function getItemDisplayContent(item: ListItem): string { if (item.content.includes('#')) { const cleaned = item.content.replace(/#([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '').replace(/\s+/g, ' ').trim(); if (cleaned) return cleaned; } return item.content; } export function deduplicateItems(items: ListItem[]): ListItem[] { const seen = new Set(); const result: ListItem[] = []; for (const item of items) { if (item && item.id && !seen.has(item.id)) { seen.add(item.id); result.push(item); } } return result; } export const SharedListsView: React.FC = ({ onRefreshGlobal, currentUser }) => { const { setGroups, setPrimaryAction } = useMobileActions(); const [lists, setLists] = useState([]); const [activeListId, setActiveListId] = useState(''); const [newItemText, setNewItemText] = useState(''); const [newItemQty, setNewItemQty] = useState(''); const [isSubmittingItem, setIsSubmittingItem] = useState(false); const [wsConnected, setWsConnected] = useState(false); const [showNewListModal, setShowNewListModal] = useState(false); const [newListTitle, setNewListTitle] = useState(''); const [newListIcon, setNewListIcon] = useState('🛒'); const [showShareModal, setShowShareModal] = useState(false); const [collaborators, setCollaborators] = useState([]); const [teams, setTeams] = useState([]); const [cursorPosition, setCursorPosition] = useState(null); const [suggestionIndex, setSuggestionIndex] = useState(0); const [showSuggestions, setShowSuggestions] = useState(false); const [workspaceTags, setWorkspaceTags] = useState([]); const inputRef = useRef(null); const [draggingItemId, setDraggingItemId] = useState(null); const [dragOverItemTarget, setDragOverItemTarget] = useState<{ itemId: string; position: 'before' | 'after' } | null>(null); const [dragOverCategoryTarget, setDragOverCategoryTarget] = useState(null); const wsRef = useRef(null); const loadLists = async () => { try { let all = await api.getAllLists(); if (all.length === 0) { try { const created = await api.createList('Wocheneinkauf', '🛒', 'shopping'); all = [created]; } catch { // fallback } } const deduplicated = all.map((l) => ({ ...l, items: deduplicateItems(l.items || []), })); setLists(deduplicated); if (deduplicated.length > 0 && (!activeListId || !deduplicated.some((l) => l.id === activeListId))) { setActiveListId(deduplicated[0].id); } } catch { // ignore } }; useEffect(() => { loadLists(); api .getWorkspaceTags() .then((tags) => { setWorkspaceTags(tags.map((t) => t.tag.replace(/^#/, ''))); }) .catch(() => {}); }, []); const activeList = useMemo(() => { return lists.find((l) => l.id === activeListId) || lists[0] || null; }, [lists, activeListId]); useEffect(() => { if (!activeList?.id) return; if (wsRef.current) { wsRef.current.close(); } let ws: WebSocket | null = null; try { ws = new WebSocket(buildWebSocketURL(activeList.id)); wsRef.current = ws; ws.onopen = () => { setWsConnected(true); }; ws.onclose = () => { setWsConnected(false); }; ws.onmessage = (evt) => { try { const msg: WSMessage = JSON.parse(evt.data); const msgListId = msg.listId || (msg as any).list_id; if (msg.event === 'ITEM_CREATED' && msgListId) { const newItem: ListItem = msg.payload; if (!newItem || !newItem.id) return; setLists((prev) => prev.map((l) => { if (l.id !== msgListId) return l; if (l.items.some((i) => i.id === newItem.id)) return l; return { ...l, items: deduplicateItems([newItem, ...l.items]) }; }) ); } else if (msg.event === 'ITEM_TOGGLED' && msgListId) { const updatedItem: ListItem = msg.payload; setLists((prev) => prev.map((l) => { if (l.id !== msgListId) return l; return { ...l, items: l.items.map((i) => (i.id === updatedItem.id ? updatedItem : i)), }; }) ); } else if (msg.event === 'ITEM_DELETED' && msgListId) { const deletedId = msg.payload?.itemId; setLists((prev) => prev.map((l) => { if (l.id !== msgListId) return l; return { ...l, items: l.items.filter((i) => i.id !== deletedId) }; }) ); } else if (msg.event === 'LIST_CLEARED' && msgListId) { setLists((prev) => prev.map((l) => { if (l.id !== msgListId) return l; return { ...l, items: l.items.filter((i) => !i.isCompleted) }; }) ); } else if ( msg.event === 'LIST_CREATED' || msg.event === 'LIST_DELETED' || msg.event === 'LIST_SHARED' || msg.event === 'LIST_UNSHARED' || msg.event === 'ITEM_REORDERED' || msg.event === 'LIST_UPDATED' ) { loadLists(); } } catch { // ignore malformed frames } }; } catch { setWsConnected(false); } return () => { if (ws) ws.close(); }; }, [activeList?.id]); useEffect(() => { const handleGlobalWs = (e: any) => { const msg = e.detail; if (!msg) return; if ( msg.event === 'LIST_SHARED' || msg.event === 'LIST_UNSHARED' || msg.event === 'LIST_CREATED' || msg.event === 'LIST_DELETED' || msg.event === 'ITEM_REORDERED' || msg.event === 'LIST_UPDATED' ) { loadLists(); } }; window.addEventListener('mindsift:ws', handleGlobalWs); return () => window.removeEventListener('mindsift:ws', handleGlobalWs); }, []); const handleAddItem = async (e: React.FormEvent) => { e.preventDefault(); if (!activeList || !newItemText.trim() || isSubmittingItem) return; setIsSubmittingItem(true); triggerHaptic(15); setShowSuggestions(false); const parsed = parseListItemInput(newItemText, newItemQty); setNewItemText(''); setNewItemQty(''); setCursorPosition(null); try { const item = await api.createListItem(activeList.id, parsed.content, parsed.category, parsed.quantity); setLists((prev) => prev.map((l) => { if (l.id !== activeList.id) return l; if (l.items.some((i) => i.id === item.id)) return l; return { ...l, items: deduplicateItems([item, ...l.items]) }; }) ); onRefreshGlobal(); } catch (err: any) { alert(err.message || 'Fehler beim Erstellen des Listeneintrags'); } finally { setIsSubmittingItem(false); } }; const handleToggleItem = async (item: ListItem) => { if (!activeList) return; triggerHaptic(10); const nextCompleted = !item.isCompleted; setLists((prev) => prev.map((l) => { if (l.id !== activeList.id) return l; return { ...l, items: l.items.map((i) => (i.id === item.id ? { ...i, isCompleted: nextCompleted } : i)), }; }) ); await api.toggleListItem(activeList.id, item.id, nextCompleted); onRefreshGlobal(); }; const handleDeleteItem = async (itemId: string) => { if (!activeList) return; triggerHaptic(15); setLists((prev) => prev.map((l) => l.id === activeList.id ? { ...l, items: l.items.filter((i) => i.id !== itemId) } : l ) ); await api.deleteListItem(activeList.id, itemId); }; const handleClearCompleted = async () => { if (!activeList) return; triggerHaptic(20); await api.clearCompletedItems(activeList.id); setLists((prev) => prev.map((l) => l.id === activeList.id ? { ...l, items: l.items.filter((i) => !i.isCompleted) } : l ) ); }; const handleItemDragStart = (e: React.DragEvent, item: ListItem) => { e.stopPropagation(); e.dataTransfer.setData('application/mindsift-list-item-id', item.id); e.dataTransfer.setData('text/plain', item.id); e.dataTransfer.effectAllowed = 'move'; setDraggingItemId(item.id); }; const handleItemDragOver = (e: React.DragEvent, targetItemId: string) => { e.preventDefault(); e.stopPropagation(); if (!draggingItemId || draggingItemId === targetItemId) return; const rect = e.currentTarget.getBoundingClientRect(); const position = e.clientY - rect.top < rect.height / 2 ? 'before' : 'after'; if ( !dragOverItemTarget || dragOverItemTarget.itemId !== targetItemId || dragOverItemTarget.position !== position ) { setDragOverItemTarget({ itemId: targetItemId, position }); } }; const handleItemDragLeave = (e: React.DragEvent, targetItemId: string) => { e.stopPropagation(); if (dragOverItemTarget?.itemId === targetItemId) { setDragOverItemTarget(null); } }; const handleDropOnItem = async (e: React.DragEvent, targetItem: ListItem, targetCategory: string) => { e.preventDefault(); e.stopPropagation(); const sourceId = e.dataTransfer.getData('application/mindsift-list-item-id') || e.dataTransfer.getData('text/plain') || draggingItemId; const position = dragOverItemTarget?.position || 'after'; setDraggingItemId(null); setDragOverItemTarget(null); setDragOverCategoryTarget(null); if (!activeList || !sourceId || sourceId === targetItem.id) return; triggerHaptic(15); const sourceItem = activeList.items.find((i) => i.id === sourceId); if (!sourceItem) return; const targetCatNormalized = targetCategory.trim(); const isCategoryChanged = (sourceItem.category || '').toLowerCase() !== targetCatNormalized.toLowerCase(); let newCategory = sourceItem.category; let newContent = sourceItem.content; if (isCategoryChanged) { newCategory = targetCatNormalized; if (!targetCatNormalized) { newContent = newContent.replace(/#([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '').replace(/\s+/g, ' ').trim(); } else if (newContent.includes('#')) { newContent = newContent.replace(/#([a-zA-Z0-9äöüÄÖÜß_-]+)/g, `#${targetCatNormalized}`).trim(); } } const remainingItems = activeList.items.filter((i) => i.id !== sourceId); const targetIndex = remainingItems.findIndex((i) => i.id === targetItem.id); if (targetIndex === -1) return; const insertIndex = position === 'before' ? targetIndex : targetIndex + 1; const updatedSourceItem: ListItem = { ...sourceItem, category: newCategory, content: newContent, }; const newItems = [...remainingItems]; newItems.splice(insertIndex, 0, updatedSourceItem); const reorderPayload: { id: string; sortOrder: number; category?: string; content?: string }[] = []; newItems.forEach((it, idx) => { it.sortOrder = idx; if (it.id === sourceId) { reorderPayload.push({ id: it.id, sortOrder: idx, category: newCategory, content: newContent, }); } else { reorderPayload.push({ id: it.id, sortOrder: idx, }); } }); setLists((prev) => prev.map((l) => (l.id === activeList.id ? { ...l, items: newItems } : l)) ); try { await api.reorderListItems(activeList.id, reorderPayload); } catch { loadLists(); } }; const handleDropOnCategoryHeader = async (e: React.DragEvent, targetCategory: string) => { e.preventDefault(); e.stopPropagation(); const sourceId = e.dataTransfer.getData('application/mindsift-list-item-id') || e.dataTransfer.getData('text/plain') || draggingItemId; setDraggingItemId(null); setDragOverItemTarget(null); setDragOverCategoryTarget(null); if (!activeList || !sourceId) return; triggerHaptic(15); const sourceItem = activeList.items.find((i) => i.id === sourceId); if (!sourceItem) return; const targetCatNormalized = targetCategory.trim(); let newCategory = targetCatNormalized; let newContent = sourceItem.content; if (!targetCatNormalized) { newContent = newContent.replace(/#([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '').replace(/\s+/g, ' ').trim(); } else if (newContent.includes('#')) { newContent = newContent.replace(/#([a-zA-Z0-9äöüÄÖÜß_-]+)/g, `#${targetCatNormalized}`).trim(); } const updatedSourceItem: ListItem = { ...sourceItem, category: newCategory, content: newContent, }; const remainingItems = activeList.items.filter((i) => i.id !== sourceId); const newItems = [updatedSourceItem, ...remainingItems]; const reorderPayload = newItems.map((it, idx) => { it.sortOrder = idx; if (it.id === sourceId) { return { id: it.id, sortOrder: idx, category: newCategory, content: newContent, }; } return { id: it.id, sortOrder: idx }; }); setLists((prev) => prev.map((l) => (l.id === activeList.id ? { ...l, items: newItems } : l)) ); try { await api.reorderListItems(activeList.id, reorderPayload); } catch { loadLists(); } }; const handleCreateList = async (e: React.FormEvent) => { e.preventDefault(); if (!newListTitle.trim()) return; const created = await api.createList(newListTitle.trim(), newListIcon, 'shopping'); setNewListTitle(''); setShowNewListModal(false); await loadLists(); setActiveListId(created.id); onRefreshGlobal(); }; const handleDeleteList = async (listId: string) => { if (!window.confirm('Diese Liste wirklich löschen?')) return; try { await api.deleteList(listId); await loadLists(); onRefreshGlobal(); } catch (err: any) { alert(err.message || 'Liste konnte nicht gelöscht werden'); } }; const openShareModal = async () => { if (!activeList) return; setShowShareModal(true); try { const [usersList, teamsList, sharesList] = await Promise.all([ api.getUsersDirectory ? api.getUsersDirectory().catch(() => api.getCollaborators().catch(() => [])) : api.getCollaborators().catch(() => []), api.getUserTeams().catch(() => []), api.getListShares(activeList.id).catch(() => []), ]); setCollaborators(usersList); setTeams(teamsList); setLists((prev) => prev.map((l) => (l.id === activeList.id ? { ...l, shares: sharesList } : l))); } catch { // ignore } }; const allAvailableHashtags = useMemo(() => { const tagMap = new Map(); // 1. Collect from all lists and items (open and completed) for (const l of lists) { for (const item of l.items) { if (item.category && item.category.trim()) { const raw = item.category.trim().replace(/^#/, ''); if (raw) { const lower = raw.toLowerCase(); const existing = tagMap.get(lower); const formatted = raw.charAt(0).toUpperCase() + raw.slice(1); if (!existing || (raw[0] === raw[0].toUpperCase() && existing[0] === existing[0].toLowerCase())) { tagMap.set(lower, formatted); } } } if (item.content && item.content.includes('#')) { const matches = item.content.matchAll(/#([a-zA-Z0-9äöüÄÖÜß_-]+)/g); for (const m of matches) { const raw = m[1]; const lower = raw.toLowerCase(); const existing = tagMap.get(lower); const formatted = raw.charAt(0).toUpperCase() + raw.slice(1); if (!existing || (raw[0] === raw[0].toUpperCase() && existing[0] === existing[0].toLowerCase())) { tagMap.set(lower, formatted); } } } } } // 2. Also incorporate workspace tags for (const t of workspaceTags) { const raw = t.trim().replace(/^#/, ''); if (raw) { const lower = raw.toLowerCase(); if (!tagMap.has(lower)) { tagMap.set(lower, raw.charAt(0).toUpperCase() + raw.slice(1)); } } } return Array.from(tagMap.values()).sort((a, b) => a.localeCompare(b, 'de', { sensitivity: 'base' }) ); }, [lists, workspaceTags]); const activeHashtagQuery = useMemo(() => { if (!showSuggestions) return null; const pos = cursorPosition !== null ? cursorPosition : newItemText.length; const textBefore = newItemText.slice(0, pos); const match = textBefore.match(/(?:^|\s)#([a-zA-Z0-9äöüÄÖÜß_-]*)$/); if (!match) return null; return { query: match[1], matchIndex: match.index! + (match[0].startsWith(' ') ? 1 : 0), cursorPos: pos, }; }, [newItemText, cursorPosition, showSuggestions]); const matchingSuggestions = useMemo(() => { if (!activeHashtagQuery) return []; const q = activeHashtagQuery.query.toLowerCase(); if (!q) { return allAvailableHashtags; } const startsWithMatches: string[] = []; const containsMatches: string[] = []; for (const tag of allAvailableHashtags) { const lower = tag.toLowerCase(); if (lower.startsWith(q)) { startsWithMatches.push(tag); } else if (lower.includes(q)) { containsMatches.push(tag); } } return [...startsWithMatches, ...containsMatches]; }, [activeHashtagQuery, allAvailableHashtags]); const applySuggestion = (tag: string) => { if (!activeHashtagQuery) return; const before = newItemText.slice(0, activeHashtagQuery.matchIndex); const after = newItemText.slice(activeHashtagQuery.cursorPos); const newText = `${before}#${tag} ${after}`; setNewItemText(newText); setShowSuggestions(false); setSuggestionIndex(0); const newCursor = activeHashtagQuery.matchIndex + tag.length + 2; // '#' + tag + ' ' setTimeout(() => { if (inputRef.current) { inputRef.current.focus(); inputRef.current.setSelectionRange(newCursor, newCursor); setCursorPosition(newCursor); } }, 10); }; const handleInputKeyDown = (e: React.KeyboardEvent) => { if (showSuggestions && activeHashtagQuery && matchingSuggestions.length > 0) { if (e.key === 'ArrowDown') { e.preventDefault(); setSuggestionIndex((prev) => (prev + 1) % matchingSuggestions.length); return; } if (e.key === 'ArrowUp') { e.preventDefault(); setSuggestionIndex((prev) => (prev - 1 + matchingSuggestions.length) % matchingSuggestions.length); return; } if (e.key === 'Enter' || e.key === 'Tab') { e.preventDefault(); const selected = matchingSuggestions[suggestionIndex] || matchingSuggestions[0]; if (selected) { applySuggestion(selected); } return; } if (e.key === 'Escape') { e.preventDefault(); setShowSuggestions(false); return; } } }; const safeActiveItems = useMemo(() => deduplicateItems(activeList?.items || []), [activeList?.items]); const openItems = useMemo(() => safeActiveItems.filter((i) => !i.isCompleted), [safeActiveItems]); const completedItems = useMemo(() => safeActiveItems.filter((i) => i.isCompleted), [safeActiveItems]); const groupedOpenItems = useMemo(() => { const groupsMap = new Map(); const canonicalMap = new Map(); const uncategorized: ListItem[] = []; for (const item of openItems) { const cat = getItemCategory(item); if (!cat) { uncategorized.push(item); } else { const key = cat.toLowerCase(); const list = groupsMap.get(key) || []; list.push(item); groupsMap.set(key, list); const currentCanonical = canonicalMap.get(key); if (!currentCanonical) { const formatted = cat.charAt(0).toUpperCase() + cat.slice(1); canonicalMap.set(key, formatted); } else if (cat[0] === cat[0].toUpperCase() && currentCanonical[0] === currentCanonical[0].toLowerCase()) { canonicalMap.set(key, cat); } } } const sortedKeys = Array.from(groupsMap.keys()).sort((a, b) => { const labelA = canonicalMap.get(a) || a; const labelB = canonicalMap.get(b) || b; return labelA.localeCompare(labelB, 'de', { sensitivity: 'base' }); }); const result: { category: string; label: string; items: ListItem[] }[] = []; for (const key of sortedKeys) { const displayLabel = canonicalMap.get(key) || key; result.push({ category: key, label: `#${displayLabel}`, items: groupsMap.get(key)!, }); } if (uncategorized.length > 0) { result.push({ category: '', label: result.length > 0 ? 'Allgemein' : '', items: uncategorized, }); } return result; }, [openItems]); // Register context-sensitive mobile menu groups behind the MindSift logo useEffect(() => { setPrimaryAction(() => { setTimeout(() => { inputRef.current?.focus(); inputRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 60); }); setGroups([ { id: 'lists-actions', title: 'Aktionen', items: [ { id: 'list-new-item', label: 'Neuen Eintrag hinzufügen', icon: , variant: 'accent', onClick: () => { setTimeout(() => { inputRef.current?.focus(); inputRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 60); }, }, { id: 'list-new-list', label: 'Neue Einkaufsliste erstellen', icon: , onClick: () => setShowNewListModal(true), }, ...(activeList ? [ { id: 'list-share', label: `Liste "${activeList.title}" teilen`, icon: , onClick: () => openShareModal(), }, ] : []), ...(completedItems.length > 0 ? [ { id: 'list-clear-completed', label: `Erledigte aufräumen (${completedItems.length})`, icon: , onClick: () => handleClearCompleted(), }, ] : []), ...(activeList && lists.length > 1 ? [ { id: 'list-delete', label: `Liste "${activeList.title}" löschen`, icon: , variant: 'danger' as const, onClick: () => handleDeleteList(activeList.id), }, ] : []), ], }, { id: 'lists-switcher', title: 'Einkaufslisten wechseln', items: lists.map((lst) => { const openCount = deduplicateItems(lst.items || []).filter((i) => !i.isCompleted).length; return { id: `select-list-${lst.id}`, label: lst.title, icon: {lst.icon}, badge: openCount > 0 ? openCount : undefined, active: activeList?.id === lst.id, onClick: () => setActiveListId(lst.id), }; }), }, ]); }, [lists, activeList, completedItems.length, setGroups, setPrimaryAction]); return (
{/* Mobile Compact Header Bar (List switching & actions live in MindSift Logo Menu) */}
{activeList ? `${activeList.icon} ${activeList.title}` : '🛒 Einkaufsliste'} {openItems.length > 0 && ( {openItems.length} offen )}
{activeList && ( )}
{/* Top Header: List Tabs & Actions (Desktop) */}
{/* List Tabs */}
{lists.map((lst) => { const openCount = deduplicateItems(lst.items || []).filter((i) => !i.isCompleted).length; const isSelected = activeList?.id === lst.id; return (
{isSelected && lists.length > 1 && ( )}
); })}
{/* Right Header Section: Geteilt Badge, Icon-Only Share Button & Live Sync */}
{activeList && (() => { const isOwner = activeList.isOwner !== false && (activeList.userId === currentUser?.id || !activeList.userId || currentUser?.role === 'admin'); const sharesCount = activeList.shares?.length || 0; const isShared = sharesCount > 0; const ownerDisplayName = activeList.ownerName || 'Eigentümer'; return (
{isShared && ( isOwner ? ( ) : ( ) )}
); })()} {/* Live Sync Status */}
{showNewListModal && (
{['🛒', '🏔️', '🍕', '🎉', '🧰', '🎁'].map((ic) => ( ))}
setNewListTitle(e.target.value)} placeholder="Titel der Liste (z. B. Baumarkt, Drogerie, Camping)…" className="flex-1 h-9 px-3 input-field text-sm" autoFocus />
)} {/* Active Shared List View */} {activeList ? (
{/* Quick-Add Form */}
{ setNewItemText(e.target.value); setCursorPosition(e.target.selectionStart); setShowSuggestions(true); setSuggestionIndex(0); }} onKeyDown={handleInputKeyDown} onSelect={(e) => { setCursorPosition(e.currentTarget.selectionStart); }} onFocus={(e) => { setCursorPosition(e.target.selectionStart); setShowSuggestions(true); }} onBlur={() => { setTimeout(() => setShowSuggestions(false), 200); }} placeholder={`Eintrag zu "${activeList.title}" hinzufügen (z. B. Hafermilch #Kühlregal)…`} className="flex-1 bg-transparent py-1.5 sm:py-1 text-sm text-main placeholder-faint focus:outline-none" /> {/* Hashtag Autocomplete Suggestions Dropdown */} {showSuggestions && activeHashtagQuery && matchingSuggestions.length > 0 && (
Hashtags
{matchingSuggestions.map((tag, idx) => { const isSelected = idx === suggestionIndex; return ( ); })}
)}
setNewItemQty(e.target.value)} placeholder="Menge (z.B. 2x)" className="flex-1 sm:flex-none sm:w-28 h-9 sm:h-7 px-2.5 sm:px-2 input-field text-xs" />
{/* Open Items Grouped by Hashtags / Categories */} {openItems.length === 0 ? (

Alles erledigt

Keine offenen Punkte auf dieser Liste.

) : (
{groupedOpenItems.map((group) => (
{/* Category Group Header: white text directly on canvas background */} {group.label && (
{group.label} {group.items.length}
)} {/* Lighter panel-surface card container with items */}
{ e.preventDefault(); if (draggingItemId) setDragOverCategoryTarget(group.category); }} onDragLeave={() => { if (dragOverCategoryTarget === group.category) setDragOverCategoryTarget(null); }} onDrop={(e) => handleDropOnCategoryHeader(e, group.category)} className={`panel-surface divide-y divide-border overflow-hidden transition-all ${ dragOverCategoryTarget === group.category && !dragOverItemTarget ? 'ring-2 ring-accent/60 bg-accent/5' : '' }`} > {group.items.map((item) => { const cat = getItemCategory(item); const displayContent = getItemDisplayContent(item); const isDragging = draggingItemId === item.id; const isTarget = dragOverItemTarget?.itemId === item.id; return (
handleItemDragStart(e, item)} onDragEnd={() => { setDraggingItemId(null); setDragOverItemTarget(null); setDragOverCategoryTarget(null); }} onDragOver={(e) => handleItemDragOver(e, item.id)} onDragLeave={(e) => handleItemDragLeave(e, item.id)} onDrop={(e) => handleDropOnItem(e, item, group.category)} onClick={() => handleToggleItem(item)} className={`px-3.5 py-3.5 sm:px-3 sm:py-2.5 flex items-center justify-between gap-3 cursor-pointer cursor-grab active:cursor-grabbing select-none transition-colors hover:bg-panel-hover group relative ${ isDragging ? 'opacity-30 border border-dashed border-accent/40' : '' }`} > {/* Drop insertion line indicator */} {isTarget && dragOverItemTarget.position === 'before' && (
)} {isTarget && dragOverItemTarget.position === 'after' && (
)}
{/* Eckiges Abhaken */} {displayContent} {item.quantity && ( {item.quantity} )} {cat && !group.label && ( #{cat} )}
); })}
))}
)} {/* Completed Items Section */} {completedItems.length > 0 && (
Erledigt ({completedItems.length})
{completedItems.map((item) => { const displayContent = getItemDisplayContent(item); return (
handleToggleItem(item)} className="px-3.5 py-3 sm:px-3 sm:py-2 flex items-center justify-between gap-3 cursor-pointer hover:bg-panel-hover transition-colors group" >
{/* Eckiges Abhaken */} {displayContent} {item.quantity && ( ({item.quantity}) )}
{item.completedByName && ( ✓ {item.completedByName} )}
); })}
)}
) : null} {/* Unified Shopping List Sharing Modal */} {showShareModal && activeList && ( setShowShareModal(false)} title={`Liste teilen: ${activeList.title}`} resourceName={activeList.title} resourceIcon={activeList.icon || '🛒'} isOwner={activeList.isOwner !== false && (activeList.userId === currentUser?.id || !activeList.userId || currentUser?.role === 'admin')} currentUserId={currentUser?.id} shares={(activeList.shares || []).map((s) => ({ id: s.id, targetType: s.targetType, targetId: s.targetId, targetName: s.targetName || (s.targetType === 'team' ? 'Gruppe' : 'Benutzer'), targetEmail: s.targetEmail, permission: s.role === 'viewer' ? ('viewer' as const) : ('editor' as const), role: s.role, createdAt: s.createdAt, }))} teams={teams} collaborators={collaborators} allowRoleSelection={true} onAddShare={async (targetType, targetId, permission) => { await api.addListShare(activeList.id, { targetType, targetId, role: permission, }); const freshShares = await api.getListShares(activeList.id); setLists((prev) => prev.map((l) => (l.id === activeList.id ? { ...l, shares: freshShares } : l)) ); onRefreshGlobal(); }} onRemoveShare={async (shareId) => { await api.removeListShare(activeList.id, shareId); const freshShares = await api.getListShares(activeList.id); setLists((prev) => prev.map((l) => (l.id === activeList.id ? { ...l, shares: freshShares } : l)) ); onRefreshGlobal(); }} /> )}
); };