import React, { useEffect, useRef, useState } from 'react'; import { motion, useMotionValue, useTransform, AnimatePresence } from 'framer-motion'; import { X, Check, CheckSquare, Calendar, FileText, ShoppingCart, RotateCcw, Sparkles, Mic, Webhook, Keyboard, Tag, Clock, SkipForward, Edit3, Layers, MoreHorizontal, } from 'lucide-react'; import { SharedList, Thought } from '../../lib/types'; import { api, triggerHaptic } from '../../lib/api'; interface TriageDeckProps { deck: Thought[]; lists: SharedList[]; onDeckChange: () => void; onOpenDocument: (docId: string) => void; onFocusCapture: () => void; } interface SwipeableQueueItemProps { item: Thought; lists: SharedList[]; onBringToFront: (id: string) => void; onOpenActionSheet: (item: Thought) => void; onAction: ( action: 'keep' | 'discard', options?: { convertTo?: 'task' | 'document' | 'list_item'; targetListId?: string; dueDate?: string; thoughtOverride?: Thought; } ) => void; } const SwipeableQueueItem: React.FC = ({ item, lists, onBringToFront, onOpenActionSheet, onAction, }) => { const qx = useMotionValue(0); const rightBgOpacity = useTransform(qx, [20, 85], [0, 1]); const leftBgOpacity = useTransform(qx, [-85, -20], [1, 0]); const didDragRef = useRef(false); const holdTimerRef = useRef(null); const startPosRef = useRef<{ x: number; y: number } | null>(null); const clearHoldTimer = () => { if (holdTimerRef.current !== null) { window.clearTimeout(holdTimerRef.current); holdTimerRef.current = null; } }; return (
{/* Swipe Right Background Indicator (Als Aufgabe) */} Als Aufgabe 👉 {/* Swipe Left Background Indicator (Wegwerfen) */} 👈 Wegwerfen { didDragRef.current = false; startPosRef.current = { x: e.clientX, y: e.clientY }; clearHoldTimer(); holdTimerRef.current = window.setTimeout(() => { if (!didDragRef.current) { didDragRef.current = true; triggerHaptic([20, 40]); onOpenActionSheet(item); } }, 420); }} onPointerMove={(e) => { if (startPosRef.current) { const dx = Math.abs(e.clientX - startPosRef.current.x); const dy = Math.abs(e.clientY - startPosRef.current.y); if (dx > 10 || dy > 10) { clearHoldTimer(); } } }} onPointerUp={clearHoldTimer} onPointerCancel={clearHoldTimer} onDragStart={() => { didDragRef.current = true; clearHoldTimer(); }} onDragEnd={(_, info) => { clearHoldTimer(); if (info.offset.x > 85 || info.velocity.x > 450) { onAction('keep', { convertTo: 'task', thoughtOverride: item }); } else if (info.offset.x < -85 || info.velocity.x < -450) { onAction('discard', { thoughtOverride: item }); } }} onClick={() => { if (!didDragRef.current) { onBringToFront(item.id); } }} className="relative bg-panel px-4 py-3.5 sm:px-3.5 sm:py-2.5 min-h-[56px] sm:min-h-[44px] flex items-center justify-between gap-3 hover:bg-panel-hover transition-colors group cursor-pointer" >
{item.content}
{/* Desktop quick buttons (hidden on mobile so queue items are clean & easy to tap/swipe) */}
e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()} > {lists.length > 0 && ( )}
); }; export const TriageDeck: React.FC = ({ deck, lists, onDeckChange, onFocusCapture, }) => { const [cards, setCards] = useState(deck); const [lastTriaged, setLastTriaged] = useState(null); const [showDateModal, setShowDateModal] = useState(false); const [taskDueDate, setTaskDueDate] = useState(() => { const tomorrow = new Date(Date.now() + 86400000); return tomorrow.toISOString().slice(0, 10); }); const [showListSelector, setShowListSelector] = useState(false); // Long-Press Action Sheet state (for Triage Card or Queue Item) const [actionSheetCard, setActionSheetCard] = useState(null); const [sheetMode, setSheetMode] = useState<'main' | 'date' | 'list'>('main'); const cardHoldTimerRef = useRef(null); const cardPointerStartRef = useRef<{ x: number; y: number } | null>(null); // Inline edit state for cleaning up a hasty Quick Dump note before converting const [isEditingCard, setIsEditingCard] = useState(false); const [editDraft, setEditDraft] = useState(''); useEffect(() => { setCards(deck); }, [deck]); const activeCard = cards[0]; const nextCard = cards[1]; useEffect(() => { setIsEditingCard(false); setEditDraft(activeCard?.content || ''); }, [activeCard?.id]); const clearCardHoldTimer = () => { if (cardHoldTimerRef.current !== null) { window.clearTimeout(cardHoldTimerRef.current); cardHoldTimerRef.current = null; } }; const openActionSheetForCard = (card: Thought) => { setSheetMode('main'); setActionSheetCard(card); }; const x = useMotionValue(0); const rotate = useTransform(x, [-220, 0, 220], [-8, 0, 8]); const keepOpacity = useTransform(x, [25, 110], [0, 1]); const discardOpacity = useTransform(x, [-110, -25], [1, 0]); const cardBorderColor = useTransform( x, [-140, 0, 140], ['rgba(239, 68, 68, 0.7)', 'rgba(255, 255, 255, 0.08)', 'rgba(29, 140, 248, 0.8)'] ); const handleAction = async ( action: 'keep' | 'discard', options?: { convertTo?: 'task' | 'document' | 'list_item'; targetListId?: string; dueDate?: string; thoughtOverride?: Thought; } ) => { const targetCard = options?.thoughtOverride || activeCard; if (!targetCard) return; triggerHaptic(action === 'keep' ? [10, 20] : 15); // Optimistic removal from local deck setCards((prev) => prev.filter((c) => c.id !== targetCard.id)); setLastTriaged(targetCard); setShowDateModal(false); setShowListSelector(false); setActionSheetCard(null); setIsEditingCard(false); x.set(0); try { await api.triageAction(targetCard.id, action, { convertTo: options?.convertTo, targetListId: options?.targetListId, dueDate: options?.dueDate, }); onDeckChange(); } catch { onDeckChange(); } }; const handleSkipCard = (targetId?: string) => { if (cards.length <= 1) return; triggerHaptic(10); setShowDateModal(false); setShowListSelector(false); setActionSheetCard(null); setIsEditingCard(false); x.set(0); if (targetId && targetId !== cards[0]?.id) { setCards((prev) => { const chosen = prev.find((c) => c.id === targetId); if (!chosen) return prev; return [...prev.filter((c) => c.id !== targetId), chosen]; }); } else { setCards((prev) => [...prev.slice(1), prev[0]]); } }; const handleBringToFront = (thoughtId: string) => { triggerHaptic(10); setCards((prev) => { const chosen = prev.find((c) => c.id === thoughtId); if (!chosen) return prev; return [chosen, ...prev.filter((c) => c.id !== thoughtId)]; }); }; const handleSaveInlineEdit = async () => { if (!activeCard) return; const trimmed = editDraft.trim(); if (!trimmed || trimmed === activeCard.content) { setIsEditingCard(false); return; } try { const updated = await api.updateThought(activeCard.id, { content: trimmed }); setCards((prev) => prev.map((c) => (c.id === activeCard.id ? updated : c))); setIsEditingCard(false); onDeckChange(); } catch { setIsEditingCard(false); } }; const handleUndo = async () => { if (!lastTriaged) return; triggerHaptic(15); const toRestore = lastTriaged; setLastTriaged(null); await api.triageAction(toRestore.id, 'restore'); onDeckChange(); }; // Keyboard shortcuts for desktop power users useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { const target = e.target as HTMLElement; if ( target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable || !activeCard ) { return; } if (e.key === 'ArrowLeft' || e.key === 'h' || e.key === 'H') { e.preventDefault(); handleAction('discard'); } else if (e.key === 'ArrowRight' || e.key === 'l' || e.key === 'L') { e.preventDefault(); handleAction('keep', { convertTo: 'task' }); } else if (e.key === 't' || e.key === 'T') { e.preventDefault(); setShowListSelector(false); setShowDateModal((prev) => !prev); } else if (e.key === 'd' || e.key === 'D') { e.preventDefault(); handleAction('keep', { convertTo: 'document' }); } else if (e.key === ' ') { e.preventDefault(); handleSkipCard(); } else if (e.key === 'Escape' && actionSheetCard) { setActionSheetCard(null); } }; window.addEventListener('keydown', onKeyDown); return () => window.removeEventListener('keydown', onKeyDown); }, [activeCard, cards.length, actionSheetCard]); const priorityBadge = (priority: string) => { switch (priority) { case 'urgent': return ( Dringend ); case 'high': return ( Wichtig ); default: return ( Quick Dump ); } }; const sourceBadge = (source: string) => { if (source === 'voice') { return ( Diktat ); } if (source === 'webhook') { return ( Webhook ); } return null; }; if (!activeCard) { return (

Triage-Deck ist leer

Alle Quick Dump Notizen sind einsortiert oder verworfen. Tippe auf das{' '} + unten in der Mitte (oder Taste{' '} C ), um neue Gedanken reinzurotzen.

{lastTriaged && ( )}
); } return (
{/* Top Deck Status & Undo Bar */}
{cards.length} {cards.length === 1 ? 'Quick Dump' : 'Quick Dumps'} offen • ← Wegwerfen • → Aufgabe • D Notiz • Leertaste Später
{lastTriaged && ( )}
{/* Card Stack Container */}
{/* Background Preview Card */} {nextCard && (
{priorityBadge(nextCard.priority)}

{nextCard.content}

Nächster Quick Dump…
)} {/* Active Draggable & Long-Pressable Card */} { if (isEditingCard) return; cardPointerStartRef.current = { x: e.clientX, y: e.clientY }; clearCardHoldTimer(); cardHoldTimerRef.current = window.setTimeout(() => { triggerHaptic([20, 40]); openActionSheetForCard(activeCard); }, 420); }} onPointerMove={(e) => { if (cardPointerStartRef.current) { const dx = Math.abs(e.clientX - cardPointerStartRef.current.x); const dy = Math.abs(e.clientY - cardPointerStartRef.current.y); if (dx > 12 || dy > 12) { clearCardHoldTimer(); } } }} onPointerUp={clearCardHoldTimer} onPointerCancel={clearCardHoldTimer} onDragStart={clearCardHoldTimer} onDragEnd={(_, info) => { clearCardHoldTimer(); if (info.offset.x > 105 || info.velocity.x > 520) { handleAction('keep', { convertTo: 'task' }); } else if (info.offset.x < -105 || info.velocity.x < -520) { handleAction('discard'); } }} initial={{ scale: 0.97, opacity: 0, y: 10 }} animate={{ scale: 1, opacity: 1, y: 0 }} exit={{ scale: 0.93, opacity: 0 }} className="relative w-full h-full panel-surface p-5 cursor-grab active:cursor-grabbing flex flex-col justify-between overflow-hidden" > {/* Dynamic Swipe Overlays */} Als Aufgabe 👉 👈 Wegwerfen {/* Card Header */}
{priorityBadge(activeCard.priority)} {sourceBadge(activeCard.source)}
e.stopPropagation()} > {new Date(activeCard.createdAt).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit', })}
{/* Card Body */}
{ if (isEditingCard) e.stopPropagation(); }} > {isEditingCard ? (