First draft
This commit is contained in:
1 parent
aba52f2b3c
commit
84bc6884b0
37 files changed
+6697
-27
No files matched your search
@@ -0,0 +1,587 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Plus,
|
||||
CheckCircle2,
|
||||
Circle,
|
||||
Trash2,
|
||||
Users,
|
||||
Copy,
|
||||
Check,
|
||||
Radio,
|
||||
Sparkles,
|
||||
UserPlus,
|
||||
} from 'lucide-react';
|
||||
import { ListItem, SharedList, Space, WSMessage } from '../../lib/types';
|
||||
import { api, buildWebSocketURL, triggerHaptic } from '../../lib/api';
|
||||
|
||||
interface SharedListsViewProps {
|
||||
onRefreshGlobal: () => void;
|
||||
}
|
||||
|
||||
const CATEGORIES = ['Obst & Gemüse', 'Kühlregal', 'Vorrat & Konserven', 'Backwaren', 'Getränke', 'Drogerie', 'Sonstiges'];
|
||||
|
||||
export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGlobal }) => {
|
||||
const [spaces, setSpaces] = useState<Space[]>([]);
|
||||
const [activeSpace, setActiveSpace] = useState<Space | null>(null);
|
||||
const [lists, setLists] = useState<SharedList[]>([]);
|
||||
const [activeListId, setActiveListId] = useState<string>('');
|
||||
|
||||
const [newItemText, setNewItemText] = useState('');
|
||||
const [newItemQty, setNewItemQty] = useState('');
|
||||
const [newItemCategory, setNewItemCategory] = useState('');
|
||||
|
||||
const [wsConnected, setWsConnected] = useState(false);
|
||||
const [onlineUsers, setOnlineUsers] = useState(1);
|
||||
const [copiedInvite, setCopiedInvite] = useState(false);
|
||||
|
||||
const [showNewListModal, setShowNewListModal] = useState(false);
|
||||
const [newListTitle, setNewListTitle] = useState('');
|
||||
const [newListIcon, setNewListIcon] = useState('🛒');
|
||||
|
||||
const [showJoinModal, setShowJoinModal] = useState(false);
|
||||
const [joinCode, setJoinCode] = useState('');
|
||||
const [showNewSpaceModal, setShowNewSpaceModal] = useState(false);
|
||||
const [newSpaceName, setNewSpaceName] = useState('');
|
||||
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
|
||||
const loadSpaces = async () => {
|
||||
const sp = await api.getSpaces();
|
||||
setSpaces(sp);
|
||||
if (sp.length > 0 && !activeSpace) {
|
||||
setActiveSpace(sp[0]);
|
||||
}
|
||||
};
|
||||
|
||||
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 === 'USER_PRESENCE' && msg.payload?.onlineCount) {
|
||||
setOnlineUsers(msg.payload.onlineCount);
|
||||
} else 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);
|
||||
}
|
||||
} catch {
|
||||
// ignore malformed frames
|
||||
}
|
||||
};
|
||||
} catch {
|
||||
setWsConnected(false);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (ws) ws.close();
|
||||
};
|
||||
}, [activeSpace?.id]);
|
||||
|
||||
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 text = newItemText.trim();
|
||||
const qty = newItemQty.trim();
|
||||
const cat = newItemCategory;
|
||||
|
||||
setNewItemText('');
|
||||
setNewItemQty('');
|
||||
|
||||
const created = await api.createListItem(activeList.id, text, cat, qty);
|
||||
setLists((prev) =>
|
||||
prev.map((l) => {
|
||||
if (l.id !== activeList.id) return l;
|
||||
if (l.items.some((i) => i.id === created.id)) return l;
|
||||
return { ...l, items: [created, ...l.items] };
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleToggleItem = async (item: ListItem) => {
|
||||
if (!activeList) return;
|
||||
triggerHaptic(18);
|
||||
const nextCompleted = !item.isCompleted;
|
||||
|
||||
// Optimistic UI update (BRAIN.md Section 3)
|
||||
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
|
||||
),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
const updated = await api.toggleListItem(activeList.id, item.id, nextCompleted);
|
||||
setLists((prev) =>
|
||||
prev.map((l) => {
|
||||
if (l.id !== activeList.id) return l;
|
||||
return {
|
||||
...l,
|
||||
items: l.items.map((i) => (i.id === item.id ? updated : i)),
|
||||
};
|
||||
})
|
||||
);
|
||||
} catch {
|
||||
// Rollback on error
|
||||
loadListsForSpace(activeSpace!.id);
|
||||
}
|
||||
};
|
||||
|
||||
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 handleJoinSpace = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!joinCode.trim()) return;
|
||||
try {
|
||||
const joined = await api.joinSpace(joinCode.trim());
|
||||
setJoinCode('');
|
||||
setShowJoinModal(false);
|
||||
const sp = await api.getSpaces();
|
||||
setSpaces(sp);
|
||||
setActiveSpace(joined);
|
||||
} catch (err: any) {
|
||||
alert(err.message || 'Einladungscode ungültig');
|
||||
}
|
||||
};
|
||||
|
||||
const copyInviteCode = () => {
|
||||
if (!activeSpace) return;
|
||||
navigator.clipboard.writeText(activeSpace.inviteCode);
|
||||
setCopiedInvite(true);
|
||||
setTimeout(() => setCopiedInvite(false), 2000);
|
||||
};
|
||||
|
||||
const openItems = activeList?.items.filter((i) => !i.isCompleted) || [];
|
||||
const completedItems = activeList?.items.filter((i) => i.isCompleted) || [];
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
{/* Top Space Bar & Realtime Indicator */}
|
||||
<div className="p-4 sm:p-5 rounded-3xl bg-[#1e1f20] gemini-border flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{spaces.map((sp) => (
|
||||
<button
|
||||
key={sp.id}
|
||||
onClick={() => setActiveSpace(sp)}
|
||||
className={`h-10 px-4 rounded-full text-xs sm:text-sm font-medium flex items-center gap-2 transition-all ${
|
||||
activeSpace?.id === sp.id
|
||||
? 'bg-[#8ab4f8] text-[#0b0f19] font-semibold shadow-sm'
|
||||
: 'bg-[#131314] text-[#9aa0a6] hover:text-[#e3e3e3] gemini-border'
|
||||
}`}
|
||||
>
|
||||
<span>{sp.icon}</span>
|
||||
<span>{sp.name}</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
<button
|
||||
onClick={() => setShowNewSpaceModal((prev) => !prev)}
|
||||
className="h-10 px-3.5 rounded-full bg-[#131314] hover:bg-[#282a2c] gemini-border text-xs text-[#8ab4f8] flex items-center gap-1"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" /> Space
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setShowJoinModal((prev) => !prev)}
|
||||
className="h-10 px-3.5 rounded-full bg-[#131314] hover:bg-[#282a2c] gemini-border text-xs text-[#c58af9] flex items-center gap-1"
|
||||
>
|
||||
<UserPlus className="w-3.5 h-3.5" /> Code einlösen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Realtime Status & Invite Code Pill */}
|
||||
{activeSpace && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`h-8 px-3 rounded-full text-xs flex items-center gap-1.5 ${
|
||||
wsConnected
|
||||
? 'bg-[#81c995]/15 text-[#81c995] border border-[#81c995]/30'
|
||||
: 'bg-[#282a2c] text-[#9aa0a6]'
|
||||
}`}
|
||||
>
|
||||
<Radio className={`w-3.5 h-3.5 ${wsConnected ? 'animate-pulse' : ''}`} />
|
||||
<span>{wsConnected ? `Echtzeit-Sync (${onlineUsers} aktiv)` : 'Offline-Puffer'}</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={copyInviteCode}
|
||||
title="Einladungscode für Partner oder Familie kopieren"
|
||||
className="h-8 px-3 rounded-full bg-[#131314] hover:bg-[#282a2c] gemini-border text-xs text-[#e3e3e3] flex items-center gap-1.5"
|
||||
>
|
||||
<Users className="w-3.5 h-3.5 text-[#8ab4f8]" />
|
||||
<span>Code: {activeSpace.inviteCode}</span>
|
||||
{copiedInvite ? (
|
||||
<Check className="w-3.5 h-3.5 text-[#81c995]" />
|
||||
) : (
|
||||
<Copy className="w-3 h-3 text-[#9aa0a6]" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Create or Join Space Modals */}
|
||||
{showNewSpaceModal && (
|
||||
<form
|
||||
onSubmit={handleCreateSpace}
|
||||
className="p-4 rounded-3xl bg-[#131314] gemini-border flex items-center gap-3"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={newSpaceName}
|
||||
onChange={(e) => setNewSpaceName(e.target.value)}
|
||||
placeholder="Name des neuen Familien-/Team-Spaces (z. B. Sommerurlaub 2027)…"
|
||||
className="flex-1 h-10 px-4 rounded-full bg-[#1e1f20] text-sm text-[#e3e3e3] gemini-border focus:outline-none"
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="h-10 px-5 rounded-full bg-[#8ab4f8] text-[#0b0f19] font-semibold text-xs sm:text-sm"
|
||||
>
|
||||
Space anlegen
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{showJoinModal && (
|
||||
<form
|
||||
onSubmit={handleJoinSpace}
|
||||
className="p-4 rounded-3xl bg-[#131314] gemini-border flex items-center gap-3"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={joinCode}
|
||||
onChange={(e) => setJoinCode(e.target.value)}
|
||||
placeholder="Invite-Code des Partners eingeben…"
|
||||
className="flex-1 h-10 px-4 rounded-full bg-[#1e1f20] text-sm text-[#e3e3e3] gemini-border focus:outline-none uppercase"
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="h-10 px-5 rounded-full bg-[#c58af9] text-[#0b0f19] font-semibold text-xs sm:text-sm"
|
||||
>
|
||||
Beitreten
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* List Tabs within Active Space */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{lists.map((lst) => {
|
||||
const openCount = lst.items.filter((i) => !i.isCompleted).length;
|
||||
const isSelected = activeList?.id === lst.id;
|
||||
return (
|
||||
<button
|
||||
key={lst.id}
|
||||
onClick={() => setActiveListId(lst.id)}
|
||||
className={`h-11 px-5 rounded-full text-sm font-medium flex items-center gap-2 transition-all ${
|
||||
isSelected
|
||||
? 'bg-[#1e1f20] text-[#8ab4f8] border border-[#8ab4f8]/50 shadow-gemini-glow'
|
||||
: 'bg-[#131314] text-[#9aa0a6] hover:text-[#e3e3e3] gemini-border'
|
||||
}`}
|
||||
>
|
||||
<span>{lst.icon}</span>
|
||||
<span>{lst.title}</span>
|
||||
<span className="px-2 py-0.5 rounded-full bg-[#0b0f19] text-xs text-[#8ab4f8]">
|
||||
{openCount}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
<button
|
||||
onClick={() => setShowNewListModal((prev) => !prev)}
|
||||
className="h-11 px-4 rounded-full bg-[#131314] hover:bg-[#1e1f20] gemini-border text-xs sm:text-sm text-[#e3e3e3] flex items-center gap-1.5"
|
||||
>
|
||||
<Plus className="w-4 h-4 text-[#8ab4f8]" />
|
||||
<span>Neue Liste</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showNewListModal && (
|
||||
<form
|
||||
onSubmit={handleCreateList}
|
||||
className="p-4 rounded-3xl bg-[#131314] gemini-border flex flex-wrap items-center gap-3"
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{['🛒', '🏔️', '🍕', '🎉', '🧰', '🎁'].map((ic) => (
|
||||
<button
|
||||
key={ic}
|
||||
type="button"
|
||||
onClick={() => setNewListIcon(ic)}
|
||||
className={`w-9 h-9 rounded-full flex items-center justify-center ${
|
||||
newListIcon === ic ? 'bg-[#8ab4f8]/25 border border-[#8ab4f8]' : 'bg-[#1e1f20]'
|
||||
}`}
|
||||
>
|
||||
{ic}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={newListTitle}
|
||||
onChange={(e) => setNewListTitle(e.target.value)}
|
||||
placeholder="Titel der Liste (z. B. Baumarkt, Drogerie, Camping)…"
|
||||
className="flex-1 h-10 px-4 rounded-full bg-[#1e1f20] text-sm text-[#e3e3e3] gemini-border focus:outline-none"
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="h-10 px-5 rounded-full bg-[#8ab4f8] text-[#0b0f19] font-semibold text-xs sm:text-sm"
|
||||
>
|
||||
Liste erstellen
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* Active Shared List Card */}
|
||||
{activeList ? (
|
||||
<div className="p-6 sm:p-8 rounded-3xl bg-[#1e1f20] gemini-border shadow-card-depth">
|
||||
{/* Quick-Add Pill Form */}
|
||||
<form onSubmit={handleAddItem} className="space-y-3 mb-6">
|
||||
<div className="flex flex-col sm:flex-row gap-2.5">
|
||||
<input
|
||||
type="text"
|
||||
value={newItemText}
|
||||
onChange={(e) => setNewItemText(e.target.value)}
|
||||
placeholder={`Eintrag zu "${activeList.title}" hinzufügen (z. B. Hafermilch)…`}
|
||||
className="flex-1 h-12 px-5 rounded-full bg-[#131314] text-sm sm:text-base text-[#e3e3e3] placeholder-[#9aa0a6] gemini-border focus:outline-none focus:border-[#8ab4f8]"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newItemQty}
|
||||
onChange={(e) => setNewItemQty(e.target.value)}
|
||||
placeholder="Menge (z.B. 2x)"
|
||||
className="w-32 h-12 px-4 rounded-full bg-[#131314] text-sm text-[#e3e3e3] placeholder-[#9aa0a6] gemini-border focus:outline-none focus:border-[#8ab4f8]"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!newItemText.trim()}
|
||||
className="h-12 px-6 rounded-full bg-[#8ab4f8] disabled:bg-[#282a2c] text-[#0b0f19] disabled:text-[#5f6368] font-semibold text-sm flex items-center gap-1.5 transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span>Hinzufügen</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category Quick-Select Pills */}
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<span className="text-xs text-[#9aa0a6] mr-1">Kategorie:</span>
|
||||
{CATEGORIES.map((cat) => (
|
||||
<button
|
||||
key={cat}
|
||||
type="button"
|
||||
onClick={() => setNewItemCategory((prev) => (prev === cat ? '' : cat))}
|
||||
className={`px-3 py-1 rounded-full text-xs transition-colors ${
|
||||
newItemCategory === cat
|
||||
? 'bg-[#8ab4f8]/20 text-[#8ab4f8] border border-[#8ab4f8]/40'
|
||||
: 'bg-[#131314] text-[#9aa0a6] hover:text-[#e3e3e3]'
|
||||
}`}
|
||||
>
|
||||
{cat}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Open Items */}
|
||||
<div className="space-y-2">
|
||||
{openItems.length === 0 ? (
|
||||
<div className="py-8 text-center text-[#9aa0a6] text-sm flex flex-col items-center gap-2">
|
||||
<Sparkles className="w-6 h-6 text-[#81c995]" />
|
||||
<span>Alles erledigt! Keine offenen Punkte auf dieser Liste.</span>
|
||||
</div>
|
||||
) : (
|
||||
openItems.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
onClick={() => handleToggleItem(item)}
|
||||
className="p-4 rounded-2xl bg-[#131314] hover:bg-[#242628] gemini-border flex items-center justify-between gap-3 cursor-pointer transition-all active:scale-[0.99]"
|
||||
>
|
||||
<div className="flex items-center gap-3.5 min-w-0">
|
||||
<Circle className="w-5 h-5 text-[#8ab4f8] flex-shrink-0" />
|
||||
<span className="text-base font-medium text-[#e3e3e3] truncate">
|
||||
{item.content}
|
||||
</span>
|
||||
{item.quantity && (
|
||||
<span className="px-2.5 py-0.5 rounded-full bg-[#1e1f20] text-[#8ab4f8] text-xs font-medium">
|
||||
{item.quantity}
|
||||
</span>
|
||||
)}
|
||||
{item.category && (
|
||||
<span className="hidden sm:inline px-2.5 py-0.5 rounded-full bg-[#1e1f20] text-[#9aa0a6] text-xs">
|
||||
{item.category}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteItem(item.id);
|
||||
}}
|
||||
className="w-8 h-8 rounded-full hover:bg-[#f28b82]/20 text-[#5f6368] hover:text-[#f28b82] flex items-center justify-center"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Completed Items Section */}
|
||||
{completedItems.length > 0 && (
|
||||
<div className="mt-8 pt-6 border-t border-white/[0.06]">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-[#9aa0a6]">
|
||||
Bereits im Einkaufswagen / Erledigt ({completedItems.length})
|
||||
</span>
|
||||
<button
|
||||
onClick={handleClearCompleted}
|
||||
className="px-3.5 py-1 rounded-full bg-[#131314] hover:bg-[#f28b82]/20 text-xs text-[#9aa0a6] hover:text-[#f28b82] transition-colors"
|
||||
>
|
||||
Erledigte aufräumen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 opacity-70">
|
||||
{completedItems.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
onClick={() => handleToggleItem(item)}
|
||||
className="p-3.5 rounded-2xl bg-[#131314]/60 gemini-border flex items-center justify-between gap-3 cursor-pointer"
|
||||
>
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<CheckCircle2 className="w-5 h-5 text-[#81c995] flex-shrink-0" />
|
||||
<span className="text-sm text-[#9aa0a6] line-through truncate">
|
||||
{item.content}
|
||||
</span>
|
||||
{item.quantity && (
|
||||
<span className="text-xs text-[#5f6368]">({item.quantity})</span>
|
||||
)}
|
||||
</div>
|
||||
{item.completedByName && (
|
||||
<span className="text-xs text-[#5f6368]">✓ {item.completedByName}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user