Dev #5
+212
-27
@@ -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);
|
||||
}}
|
||||
lists={lists}
|
||||
autoFocusTrigger={Date.now()}
|
||||
/>
|
||||
</div>
|
||||
<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);
|
||||
}}
|
||||
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>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user