import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Plus, CheckCircle2, Circle, Trash2, Users, Check, Radio, Sparkles, Share2, User as UserIcon, X, } from 'lucide-react'; import { ListItem, SharedList, Space, Team, User, WSMessage } from '../../lib/types'; import { api, buildWebSocketURL, triggerHaptic } from '../../lib/api'; 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 const SharedListsView: React.FC = ({ onRefreshGlobal, currentUser }) => { const [spaces, setSpaces] = useState([]); const [activeSpace, setActiveSpace] = useState(null); const [lists, setLists] = useState([]); const [activeListId, setActiveListId] = useState(''); const [newItemText, setNewItemText] = useState(''); const [newItemQty, setNewItemQty] = useState(''); const [wsConnected, setWsConnected] = useState(false); const [showNewListModal, setShowNewListModal] = useState(false); const [newListTitle, setNewListTitle] = useState(''); const [newListIcon, setNewListIcon] = useState('🛒'); const [showNewSpaceModal, setShowNewSpaceModal] = useState(false); const [newSpaceName, setNewSpaceName] = useState(''); const [showShareModal, setShowShareModal] = useState(false); const [collaborators, setCollaborators] = useState([]); const [teams, setTeams] = useState([]); const [addShareType, setAddShareType] = useState<'team' | 'user'>('team'); const [selectedTeamId, setSelectedTeamId] = useState(''); const [inviteTarget, setInviteTarget] = useState(''); const [inviteRole, setInviteRole] = useState<'editor' | 'viewer'>('editor'); const [invitingLoading, setInvitingLoading] = useState(false); const [shareError, setShareError] = useState(null); const wsRef = useRef(null); const loadSpaces = async () => { const sp = await api.getSpaces(); setSpaces(sp); if (sp.length > 0) { if (!activeSpace) { setActiveSpace(sp[0]); } else { const found = sp.find((s) => s.id === activeSpace.id); if (found) setActiveSpace(found); } } }; const loadListsForSpace = async (spaceId: string) => { const lst = await api.getSpaceLists(spaceId); setLists(lst); if (lst.length > 0 && (!activeListId || !lst.some((l) => l.id === activeListId))) { setActiveListId(lst[0].id); } }; useEffect(() => { loadSpaces(); }, []); useEffect(() => { if (!activeSpace) return; loadListsForSpace(activeSpace.id); // Connect Realtime WebSocket for this Space if (wsRef.current) { wsRef.current.close(); } let ws: WebSocket | null = null; try { ws = new WebSocket(buildWebSocketURL(activeSpace.id)); wsRef.current = ws; ws.onopen = () => { setWsConnected(true); }; ws.onclose = () => { setWsConnected(false); }; ws.onmessage = (evt) => { try { const msg: WSMessage = JSON.parse(evt.data); if (msg.event === 'ITEM_CREATED' && msg.list_id) { const newItem: ListItem = msg.payload; setLists((prev) => prev.map((l) => { if (l.id !== msg.list_id) return l; if (l.items.some((i) => i.id === newItem.id)) return l; return { ...l, items: [newItem, ...l.items] }; }) ); } else if (msg.event === 'ITEM_TOGGLED' && msg.list_id) { const updatedItem: ListItem = msg.payload; setLists((prev) => prev.map((l) => { if (l.id !== msg.list_id) return l; return { ...l, items: l.items.map((i) => (i.id === updatedItem.id ? updatedItem : i)), }; }) ); } else if (msg.event === 'ITEM_DELETED' && msg.list_id) { const deletedId = msg.payload?.itemId; setLists((prev) => prev.map((l) => { if (l.id !== msg.list_id) return l; return { ...l, items: l.items.filter((i) => i.id !== deletedId) }; }) ); } else if (msg.event === 'LIST_CLEARED' && msg.list_id) { const updatedList: SharedList = msg.payload; setLists((prev) => prev.map((l) => (l.id === msg.list_id ? updatedList : l))); } else if (msg.event === 'LIST_CREATED' || msg.event === 'LIST_DELETED') { loadListsForSpace(activeSpace.id); } else if (msg.event === 'MEMBER_ADDED' || msg.event === 'MEMBER_REMOVED') { loadSpaces(); } } catch { // ignore malformed frames } }; } catch { setWsConnected(false); } return () => { if (ws) ws.close(); }; }, [activeSpace?.id]); useEffect(() => { const handleGlobalWs = (e: any) => { const msg = e.detail; if (!msg) return; if ( msg.event === 'SPACE_JOINED' || msg.event === 'SPACE_LEFT' || msg.event === 'MEMBER_ADDED' || msg.event === 'MEMBER_REMOVED' ) { loadSpaces(); } }; window.addEventListener('mindsift:ws', handleGlobalWs); return () => window.removeEventListener('mindsift:ws', handleGlobalWs); }, []); const activeList = lists.find((l) => l.id === activeListId) || lists[0] || null; const handleAddItem = async (e: React.FormEvent) => { e.preventDefault(); if (!activeList || !newItemText.trim()) return; triggerHaptic(15); const parsed = parseListItemInput(newItemText, newItemQty); setNewItemText(''); setNewItemQty(''); try { const item = await api.createListItem(activeList.id, parsed.content, parsed.category, parsed.quantity); setLists((prev) => prev.map((l) => (l.id === activeList.id ? { ...l, items: [item, ...l.items] } : l)) ); onRefreshGlobal(); } catch (err: any) { alert(err.message || 'Fehler beim Erstellen des Listeneintrags'); } }; 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); await loadListsForSpace(activeSpace!.id); }; const handleCreateList = async (e: React.FormEvent) => { e.preventDefault(); if (!activeSpace || !newListTitle.trim()) return; const created = await api.createList(activeSpace.id, newListTitle.trim(), newListIcon, 'shopping'); setNewListTitle(''); setShowNewListModal(false); await loadListsForSpace(activeSpace.id); setActiveListId(created.id); onRefreshGlobal(); }; const handleCreateSpace = async (e: React.FormEvent) => { e.preventDefault(); if (!newSpaceName.trim()) return; const created = await api.createSpace(newSpaceName.trim(), '🏡', 'Geteilter Familien- & Team-Bereich'); setNewSpaceName(''); setShowNewSpaceModal(false); const sp = await api.getSpaces(); setSpaces(sp); setActiveSpace(created); }; const handleAddShare = async (e: React.FormEvent) => { e.preventDefault(); if (!activeSpace) return; setInvitingLoading(true); setShareError(null); try { if (addShareType === 'team') { if (!selectedTeamId) { setShareError('Bitte ein Team oder eine Gruppe auswählen'); setInvitingLoading(false); return; } await api.addSpaceMember(activeSpace.id, { teamId: selectedTeamId, role: inviteRole, }); setSelectedTeamId(''); } else { const target = inviteTarget.trim(); if (!target) { setShareError('Bitte einen Benutzer auswählen oder eingeben'); setInvitingLoading(false); return; } const isId = collaborators.some((u) => u.id === target); await api.addSpaceMember(activeSpace.id, { userId: isId ? target : undefined, email: !isId ? target : undefined, role: inviteRole, }); setInviteTarget(''); } const sp = await api.getSpaces(); setSpaces(sp); const updated = sp.find((s) => s.id === activeSpace.id); if (updated) setActiveSpace(updated); onRefreshGlobal(); } catch (err: any) { setShareError(err.message || 'Freigabe konnte nicht hinzugefügt werden'); } finally { setInvitingLoading(false); } }; const handleRemoveMember = async (targetUserId: string) => { if (!activeSpace) return; if (!window.confirm('Dieses Mitglied wirklich aus der Einkaufsliste / dem Bereich entfernen?')) return; setInvitingLoading(true); setShareError(null); try { await api.removeSpaceMember(activeSpace.id, targetUserId); setActiveSpace((prev) => prev ? { ...prev, members: (prev.members || []).filter((m) => m.userId !== targetUserId), } : prev ); setSpaces((prev) => prev.map((s) => s.id === activeSpace.id ? { ...s, members: (s.members || []).filter((m) => m.userId !== targetUserId), } : s ) ); onRefreshGlobal(); } catch (err: any) { setShareError(err.message || 'Mitglied konnte nicht entfernt werden'); } finally { setInvitingLoading(false); } }; const openItems = activeList?.items.filter((i) => !i.isCompleted) || []; const completedItems = activeList?.items.filter((i) => i.isCompleted) || []; const groupedOpenItems = useMemo(() => { const groupsMap = new Map(); const uncategorized: ListItem[] = []; for (const item of openItems) { const cat = getItemCategory(item); if (!cat) { uncategorized.push(item); } else { const list = groupsMap.get(cat) || []; list.push(item); groupsMap.set(cat, list); } } const sortedCategories = Array.from(groupsMap.keys()).sort((a, b) => a.localeCompare(b, 'de', { sensitivity: 'base' }) ); const result: { category: string; label: string; items: ListItem[] }[] = []; for (const cat of sortedCategories) { result.push({ category: cat, label: `#${cat}`, items: groupsMap.get(cat)!, }); } if (uncategorized.length > 0) { result.push({ category: '', label: result.length > 0 ? 'Allgemein' : '', items: uncategorized, }); } return result; }, [openItems]); return (
{/* Top Space Bar & Realtime Indicator */}
{spaces.map((sp) => ( ))}
{/* Realtime Status & Share List Button */} {activeSpace && (
)}
{/* Create Space Modal */} {showNewSpaceModal && (
setNewSpaceName(e.target.value)} placeholder="Name des neuen Familien-/Team-Spaces (z. B. Urlaub 2026)…" className="flex-1 h-9 px-3 input-field text-sm" autoFocus />
)} {/* List Tabs within Active Space */}
{lists.map((lst) => { const openCount = lst.items.filter((i) => !i.isCompleted).length; const isSelected = activeList?.id === lst.id; return ( ); })}
{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 Card */} {activeList ? (
{/* Quick-Add Form */}
setNewItemText(e.target.value)} placeholder={`Eintrag zu "${activeList.title}" hinzufügen (z. B. Hafermilch #Kühlregal oder #Kaffee)…`} className="flex-1 h-9 px-3 input-field text-sm" />
setNewItemQty(e.target.value)} placeholder="Menge (z.B. 2x)" className="w-32 h-9 px-3 input-field text-sm" />
{/* Open Items Grouped by Hashtags / Categories */}
{openItems.length === 0 ? (
Alles erledigt. Keine offenen Punkte auf dieser Liste.
) : ( groupedOpenItems.map((group) => (
{/* Category Group Header (only when categories are in use) */} {group.label && (
{group.label} {group.items.length} {group.items.length === 1 ? 'Eintrag' : 'Einträge'}
)} {group.items.map((item) => { const cat = getItemCategory(item); const displayContent = getItemDisplayContent(item); return (
handleToggleItem(item)} className="px-3 py-2 bg-canvas hover:bg-panel-elevated flex items-center justify-between gap-3 cursor-pointer transition-colors group" >
{displayContent} {item.quantity && ( {item.quantity} )} {cat && ( #{cat} )}
); })}
)) )}
{/* Completed Items Section */} {completedItems.length > 0 && (
Erledigt ({completedItems.length})
{completedItems.map((item) => { const cat = getItemCategory(item); const displayContent = getItemDisplayContent(item); return (
handleToggleItem(item)} className="px-3 py-1.5 bg-canvas/50 hover:bg-canvas/80 flex items-center justify-between gap-3 cursor-pointer transition-colors" >
{displayContent} {item.quantity && ( ({item.quantity}) )} {cat && ( #{cat} )}
{item.completedByName && ( ✓ {item.completedByName} )}
); })}
)}
) : null} {/* Space & Shopping List Sharing Modal */} {showShareModal && activeSpace && (
{/* Modal Header */}

Einkaufsliste teilen: {activeSpace.name}

{shareError && (
{shareError}
)} {/* Sharing Form (Team / Gruppe vs. Einzelner Benutzer) */}
Freigabe hinzufügen:
{addShareType === 'team' ? (
) : (
{(!inviteTarget || !collaborators.some((u) => u.id === inviteTarget)) && ( setInviteTarget(e.target.value)} className="input-field h-8 px-2.5 bg-input text-main border border-border rounded-btn focus:outline-none focus:border-accent text-xs w-full" /> )}
)}
{/* Current Space Members List */}
Aktive Freigaben & Mitglieder ({activeSpace.members?.length || 1})
{(activeSpace.members || []).map((m) => { const isOwner = m.role === 'owner' || m.userId === activeSpace.ownerId; const isMe = m.userId === currentUser?.id; const canRemove = (activeSpace.ownerId === currentUser?.id || currentUser?.role === 'admin') && !isOwner; return (
{(m.displayName || m.email || '?')[0].toUpperCase()}
{m.displayName || m.email} {isMe && (Du)}
{m.email}
{isOwner ? 'Eigentümer' : m.role === 'editor' ? 'Bearbeiter' : 'Leser'} {canRemove && ( )}
); })}
)}
); };