Dev #5

Merged
sandrodach merged 19 commits from dev into main 2026-10-26 13:35:00 +01:00
Showing only changes of commit 10d5c6d563 - Show all commits
+209 -24
View File
@@ -18,8 +18,13 @@ import {
Smartphone,
Share,
Users,
Calendar,
AlertTriangle,
Flame,
Mic,
MicOff,
} from 'lucide-react';
import { AuthStatus, DashboardStats, SharedList, Thought, User } from './lib/types';
import { AuthStatus, DashboardStats, PriorityLevel, SharedList, Thought, User } from './lib/types';
import {
api,
clearToken,
@@ -132,12 +137,93 @@ export const App: React.FC = () => {
// Mobile Navigation & Quick-Action State
const [showMobileCapture, setShowMobileCapture] = useState(false);
const [showMobileProfile, setShowMobileProfile] = useState(false);
const [mobileTriageContent, setMobileTriageContent] = useState('');
const [mobileTriagePriority, setMobileTriagePriority] = useState<PriorityLevel>('normal');
const [mobileTriageDueDate, setMobileTriageDueDate] = useState('');
const [mobileVoiceActive, setMobileVoiceActive] = useState(false);
const [isVoiceCaptured, setIsVoiceCaptured] = useState(false);
const mobileRecognitionRef = useRef<any>(null);
const toggleMobileVoice = () => {
const SpeechRecognition =
(window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
if (!SpeechRecognition) {
alert('Spracheingabe wird von diesem Browser leider nicht unterstützt.');
return;
}
if (mobileVoiceActive && mobileRecognitionRef.current) {
mobileRecognitionRef.current.stop();
setMobileVoiceActive(false);
return;
}
try {
const recognition = new SpeechRecognition();
recognition.lang = 'de-DE';
recognition.continuous = false;
recognition.interimResults = true;
recognition.onstart = () => {
setMobileVoiceActive(true);
triggerHaptic(20);
};
recognition.onresult = (event: any) => {
let transcript = '';
for (let i = 0; i < event.results.length; i++) {
transcript += event.results[i][0].transcript;
}
setMobileTriageContent((prev) => {
const base = prev.replace(/\s*\[…\]$/, '').trim();
return base ? `${base} ${transcript}` : transcript;
});
setIsVoiceCaptured(true);
};
recognition.onerror = () => setMobileVoiceActive(false);
recognition.onend = () => setMobileVoiceActive(false);
mobileRecognitionRef.current = recognition;
recognition.start();
} catch {
setMobileVoiceActive(false);
}
};
const handleMobileCreateTriage = async (e: React.FormEvent) => {
e.preventDefault();
const trimmed = mobileTriageContent.trim();
if (!trimmed) return;
try {
const thought = await api.captureThought(
trimmed,
mobileTriagePriority,
isVoiceCaptured ? 'voice' : 'manual',
mobileTriageDueDate,
{ target: 'inbox' }
);
handleCaptured(thought, 'inbox');
setMobileTriageContent('');
setMobileTriagePriority('normal');
setMobileTriageDueDate('');
setIsVoiceCaptured(false);
setShowMobileCapture(false);
triggerHaptic(20);
} catch (err: any) {
alert(`Fehler beim Speichern: ${err.message || 'Unbekannter Fehler'}`);
}
};
const openMobileTriageCapture = () => {
setMobileTriageContent('');
setMobileTriagePriority('normal');
setMobileTriageDueDate('');
setIsVoiceCaptured(false);
setMobileVoiceActive(false);
setShowMobileCapture(true);
};
const { isMenuOpen, setIsMenuOpen, closeMenu, setGroups, primaryAction, setPrimaryAction } = useMobileActions();
useEffect(() => {
closeMenu();
if (activeTab === 'triage') {
setPrimaryAction(() => setShowMobileCapture(true));
setPrimaryAction(() => openMobileTriageCapture);
setGroups([
{
id: 'triage-actions',
@@ -148,7 +234,7 @@ export const App: React.FC = () => {
label: 'Neuen Gedanken erfassen',
icon: <Plus className="w-4 h-4" />,
variant: 'accent',
onClick: () => setShowMobileCapture(true),
onClick: () => openMobileTriageCapture(),
},
{
id: 'refresh-deck',
@@ -471,7 +557,7 @@ export const App: React.FC = () => {
const handleTriggerAddButton = () => {
triggerHaptic(20);
if (window.innerWidth < 640) {
setShowMobileCapture(true);
openMobileTriageCapture();
} else {
window.scrollTo({ top: 0, behavior: 'smooth' });
setCaptureFocusCount((c) => c + 1);
@@ -970,7 +1056,7 @@ export const App: React.FC = () => {
if (primaryAction) {
primaryAction();
} else {
setShowMobileCapture(true);
openMobileTriageCapture();
}
}}
className="w-14 h-14 -mt-6 rounded-full bg-accent text-on-accent shadow-lg flex items-center justify-center active:scale-90 transition-transform border-4 border-panel cursor-pointer"
@@ -1020,7 +1106,7 @@ export const App: React.FC = () => {
</button>
</nav>
{/* MOBILE QUICK-ADD BOTTOM SHEET / MODAL */}
{/* MOBILE QUICK-ADD BOTTOM SHEET / MODAL (DESIGN-ALIGNED WITH TASKS VIEW) */}
{showMobileCapture && (
<div
className="sm:hidden fixed inset-0 !m-0 z-50 flex items-end justify-center bg-black/60 backdrop-blur-xs animate-in fade-in duration-150"
@@ -1033,37 +1119,136 @@ export const App: React.FC = () => {
{/* Header */}
<div className="flex items-center justify-between pb-3 border-b border-border mb-3">
<div className="flex items-center gap-2">
<div className="w-6 h-6 rounded-btn bg-accent/15 text-accent flex items-center justify-center">
{activeTab === 'triage' ? <Layers className="w-4 h-4" /> : <Plus className="w-4 h-4" />}
<div className="w-7 h-7 rounded-btn bg-accent/15 text-accent flex items-center justify-center">
<Layers className="w-4 h-4" />
</div>
<h3 className="text-sm font-semibold text-main">
{activeTab === 'triage' ? 'Quick Dump erfassen' : 'Schnell erfassen'}
</h3>
<h3 className="text-sm font-semibold text-main">Quick Dump erfassen</h3>
</div>
<button
type="button"
onClick={() => setShowMobileCapture(false)}
className="p-1 text-faint hover:text-main rounded-btn transition-colors cursor-pointer"
className="p-1.5 text-faint hover:text-main rounded-btn transition-colors cursor-pointer"
>
<X className="w-5 h-5" />
</button>
</div>
{/* InstantCaptureBar */}
<div className="pt-1">
<InstantCaptureBar
inlineOptions={true}
defaultShowOptions={true}
hideTargetPicker={activeTab === 'triage'}
fixedTarget={activeTab === 'triage' ? 'inbox' : undefined}
onCaptured={(thought, target) => {
handleCaptured(thought, target);
setShowMobileCapture(false);
<form onSubmit={handleMobileCreateTriage} className="space-y-3">
{/* Text Input with Voice Dictation */}
<div>
<label className="text-[11px] font-semibold text-muted block mb-1">
Gedanke / Quick Dump
</label>
<div className="relative">
<input
type="text"
autoFocus
value={mobileTriageContent}
onChange={(e) => setMobileTriageContent(e.target.value)}
placeholder="Was geht dir durch den Kopf?…"
className="w-full input-field px-3 py-2 pr-10 text-sm"
/>
<button
type="button"
onClick={toggleMobileVoice}
className={`absolute right-1.5 top-1/2 -translate-y-1/2 w-7 h-7 rounded-full flex items-center justify-center transition-colors cursor-pointer ${
mobileVoiceActive
? 'bg-accent text-on-accent animate-pulse'
: 'text-muted hover:text-main hover:bg-panel-elevated'
}`}
title="Spracheingabe"
>
{mobileVoiceActive ? <MicOff className="w-3.5 h-3.5" /> : <Mic className="w-3.5 h-3.5" />}
</button>
</div>
</div>
{/* Priority - 4 Buttons (Identical to TasksView) */}
<div className="space-y-1.5 pt-1">
<label className="text-[11px] font-semibold text-muted block">Priorität</label>
<div className="grid grid-cols-4 gap-1.5">
{(['low', 'normal', 'high', 'urgent'] as PriorityLevel[]).map((p) => {
const config: Record<
PriorityLevel,
{ label: string; icon?: React.ReactNode; activeClass: string }
> = {
low: {
label: 'Niedrig',
activeClass: 'bg-canvas border-strong text-main font-semibold shadow-xs',
},
normal: {
label: 'Normal',
activeClass: 'bg-accent/15 border-accent text-accent font-semibold shadow-xs',
},
high: {
label: 'Wichtig',
icon: <AlertTriangle className="w-3 h-3 text-status-warn" />,
activeClass:
'bg-status-warn/15 border-status-warn text-status-warn font-semibold shadow-xs',
},
urgent: {
label: 'Dringend',
icon: <Flame className="w-3 h-3 text-status-err" />,
activeClass:
'bg-status-err/15 border-status-err text-status-err font-semibold shadow-xs',
},
};
const active = mobileTriagePriority === p;
const item = config[p];
return (
<button
key={p}
type="button"
onClick={() => {
triggerHaptic(10);
setMobileTriagePriority(p);
}}
lists={lists}
autoFocusTrigger={Date.now()}
className={`h-9 px-1 text-xs rounded-btn font-medium transition-colors border flex items-center justify-center gap-1 cursor-pointer ${
active
? item.activeClass
: 'bg-canvas border-border text-muted hover:text-main'
}`}
>
{item.icon}
<span className="truncate">{item.label}</span>
</button>
);
})}
</div>
</div>
{/* Due Date (Optional) */}
<div className="space-y-1 pt-1">
<label className="text-[11px] font-semibold text-muted flex items-center gap-1">
<Calendar className="w-3.5 h-3.5 text-accent" />
<span>Fällig am (optional)</span>
</label>
<input
type="date"
value={mobileTriageDueDate}
onChange={(e) => setMobileTriageDueDate(e.target.value)}
className="w-full input-field px-3 py-2 text-xs"
/>
</div>
{/* Bottom Buttons */}
<div className="pt-2 flex justify-end gap-2">
<button
type="button"
onClick={() => setShowMobileCapture(false)}
className="btn-ghost h-10 px-4 text-xs cursor-pointer"
>
Abbrechen
</button>
<button
type="submit"
disabled={!mobileTriageContent.trim()}
className="btn-primary h-10 px-5 text-xs font-semibold disabled:opacity-40 cursor-pointer"
>
Gedanke erfassen
</button>
</div>
</form>
</div>
</div>
)}