- Add saving guard to prevent duplicate task creation on rapid taps - Add pending subtasks UI to new task screen with local add/remove - Wrap both task screens in KeyboardAvoidingView with scrollToEnd on subtask focus Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e6ac92e745
commit
72eafbd9d9
2 changed files with 97 additions and 19 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
|
|
@ -8,6 +8,7 @@ import {
|
|||
useColorScheme,
|
||||
Alert,
|
||||
Platform,
|
||||
KeyboardAvoidingView,
|
||||
} from 'react-native';
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import {
|
||||
|
|
@ -79,6 +80,8 @@ export default function TaskDetailScreen() {
|
|||
const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]);
|
||||
const [lists, setLists] = useState<{ id: string; name: string; color: string | null; icon: string | null; isInbox: boolean }[]>([]);
|
||||
const [selectedListId, setSelectedListId] = useState<string>('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const scrollRef = useRef<ScrollView>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isValidUUID(id)) {
|
||||
|
|
@ -112,17 +115,23 @@ export default function TaskDetailScreen() {
|
|||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (saving) return;
|
||||
if (!task || !title.trim()) return;
|
||||
await updateTask(task.id, {
|
||||
title: title.trim(),
|
||||
notes: notes.trim() || undefined,
|
||||
priority,
|
||||
dueDate,
|
||||
recurrence,
|
||||
listId: selectedListId,
|
||||
});
|
||||
await setTagsForTask(task.id, selectedTagIds);
|
||||
router.back();
|
||||
setSaving(true);
|
||||
try {
|
||||
await updateTask(task.id, {
|
||||
title: title.trim(),
|
||||
notes: notes.trim() || undefined,
|
||||
priority,
|
||||
dueDate,
|
||||
recurrence,
|
||||
listId: selectedListId,
|
||||
});
|
||||
await setTagsForTask(task.id, selectedTagIds);
|
||||
router.back();
|
||||
} catch {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
|
|
@ -196,13 +205,14 @@ export default function TaskDetailScreen() {
|
|||
<Pressable onPress={handleDelete} className="mr-3 p-1">
|
||||
<Trash2 size={20} color={colors.terracotta.DEFAULT} />
|
||||
</Pressable>
|
||||
<Pressable onPress={handleSave} className="rounded-lg bg-bleu px-4 py-1.5">
|
||||
<Pressable onPress={handleSave} disabled={saving} className={`rounded-lg bg-bleu px-4 py-1.5 ${saving ? 'opacity-50' : ''}`}>
|
||||
<Text className="text-sm text-white" style={{ fontFamily: 'Inter_600SemiBold' }}>{t('common.save')}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<ScrollView className="flex-1 px-4 pt-4" keyboardShouldPersistTaps="handled">
|
||||
<KeyboardAvoidingView className="flex-1" behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
|
||||
<ScrollView ref={scrollRef} className="flex-1 px-4 pt-4" keyboardShouldPersistTaps="handled">
|
||||
{/* Title */}
|
||||
<TextInput
|
||||
value={title}
|
||||
|
|
@ -391,6 +401,7 @@ export default function TaskDetailScreen() {
|
|||
value={newSubtask}
|
||||
onChangeText={setNewSubtask}
|
||||
onSubmitEditing={handleAddSubtask}
|
||||
onFocus={() => setTimeout(() => scrollRef.current?.scrollToEnd({ animated: true }), 300)}
|
||||
placeholder={t('task.addSubtask')}
|
||||
placeholderTextColor={isDark ? '#A0A0A0' : '#6B6B6B'}
|
||||
className={`ml-2 flex-1 text-base ${isDark ? 'text-[#F5F5F5]' : 'text-[#1A1A1A]'}`}
|
||||
|
|
@ -398,8 +409,9 @@ export default function TaskDetailScreen() {
|
|||
/>
|
||||
</View>
|
||||
|
||||
<View className="h-24" />
|
||||
<View className="h-32" />
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
|
|
@ -7,10 +7,11 @@ import {
|
|||
ScrollView,
|
||||
useColorScheme,
|
||||
Platform,
|
||||
KeyboardAvoidingView,
|
||||
} from 'react-native';
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import {
|
||||
X, Calendar, Repeat,
|
||||
X, Calendar, Repeat, Plus,
|
||||
List, ShoppingCart, Briefcase, Home, Heart, Star, BookOpen,
|
||||
GraduationCap, Dumbbell, Utensils, Plane, Music, Code, Wrench,
|
||||
Gift, Camera, Palette, Dog, Leaf, Zap,
|
||||
|
|
@ -55,6 +56,10 @@ export default function NewTaskScreen() {
|
|||
const [recurrence, setRecurrence] = useState<string | null>(null);
|
||||
const [availableTags, setAvailableTags] = useState<{ id: string; name: string; color: string }[]>([]);
|
||||
const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [pendingSubtasks, setPendingSubtasks] = useState<string[]>([]);
|
||||
const [newSubtask, setNewSubtask] = useState('');
|
||||
const scrollRef = useRef<ScrollView>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getAllLists().then(setLists);
|
||||
|
|
@ -62,7 +67,9 @@ export default function NewTaskScreen() {
|
|||
}, []);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (saving) return;
|
||||
if (!title.trim()) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
const taskId = await createTask({
|
||||
title: title.trim(),
|
||||
|
|
@ -75,9 +82,13 @@ export default function NewTaskScreen() {
|
|||
if (selectedTagIds.length > 0) {
|
||||
await setTagsForTask(taskId, selectedTagIds);
|
||||
}
|
||||
for (const sub of pendingSubtasks) {
|
||||
await createTask({ title: sub, listId: selectedListId, parentId: taskId });
|
||||
}
|
||||
router.back();
|
||||
} catch {
|
||||
// FK constraint or other DB error — fallback to inbox
|
||||
setSaving(false);
|
||||
setSelectedListId(getInboxId());
|
||||
}
|
||||
};
|
||||
|
|
@ -87,6 +98,16 @@ export default function NewTaskScreen() {
|
|||
if (date) setDueDate(date);
|
||||
};
|
||||
|
||||
const handleAddPendingSubtask = () => {
|
||||
if (!newSubtask.trim()) return;
|
||||
setPendingSubtasks((prev) => [...prev, newSubtask.trim()]);
|
||||
setNewSubtask('');
|
||||
};
|
||||
|
||||
const handleRemovePendingSubtask = (index: number) => {
|
||||
setPendingSubtasks((prev) => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const toggleTag = (tagId: string) => {
|
||||
setSelectedTagIds((prev) =>
|
||||
prev.includes(tagId) ? prev.filter((id) => id !== tagId) : [...prev, tagId]
|
||||
|
|
@ -110,14 +131,15 @@ export default function NewTaskScreen() {
|
|||
>
|
||||
{t('task.newTask')}
|
||||
</Text>
|
||||
<Pressable onPress={handleSave} className="rounded-lg bg-bleu px-4 py-1.5">
|
||||
<Pressable onPress={handleSave} disabled={saving} className={`rounded-lg bg-bleu px-4 py-1.5 ${saving ? 'opacity-50' : ''}`}>
|
||||
<Text className="text-sm text-white" style={{ fontFamily: 'Inter_600SemiBold' }}>
|
||||
{t('common.save')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<ScrollView className="flex-1 px-4 pt-4" keyboardShouldPersistTaps="handled">
|
||||
<KeyboardAvoidingView className="flex-1" behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
|
||||
<ScrollView ref={scrollRef} className="flex-1 px-4 pt-4" keyboardShouldPersistTaps="handled">
|
||||
{/* Title */}
|
||||
<TextInput
|
||||
autoFocus
|
||||
|
|
@ -314,8 +336,52 @@ export default function NewTaskScreen() {
|
|||
</>
|
||||
)}
|
||||
|
||||
<View className="h-24" />
|
||||
{/* Subtasks */}
|
||||
<Text
|
||||
className={`mb-2 mt-6 text-xs uppercase tracking-wide ${isDark ? 'text-[#A0A0A0]' : 'text-[#6B6B6B]'}`}
|
||||
style={{ fontFamily: 'Inter_600SemiBold' }}
|
||||
>
|
||||
{t('task.subtasks')}
|
||||
</Text>
|
||||
{pendingSubtasks.map((sub, index) => (
|
||||
<View
|
||||
key={index}
|
||||
className={`flex-row items-center border-b py-2.5 ${isDark ? 'border-[#3A3A3A]' : 'border-[#E5E7EB]'}`}
|
||||
>
|
||||
<View
|
||||
className="mr-3 h-5 w-5 items-center justify-center rounded-full border-2"
|
||||
style={{ borderColor: colors.priority.none, backgroundColor: 'transparent' }}
|
||||
/>
|
||||
<Text
|
||||
className={`flex-1 text-base ${isDark ? 'text-[#F5F5F5]' : 'text-[#1A1A1A]'}`}
|
||||
style={{ fontFamily: 'Inter_400Regular' }}
|
||||
>
|
||||
{sub}
|
||||
</Text>
|
||||
<Pressable onPress={() => handleRemovePendingSubtask(index)} className="p-1">
|
||||
<X size={16} color={isDark ? '#A0A0A0' : '#6B6B6B'} />
|
||||
</Pressable>
|
||||
</View>
|
||||
))}
|
||||
|
||||
{/* Add subtask */}
|
||||
<View className="mt-2 flex-row items-center">
|
||||
<Plus size={18} color={colors.bleu.DEFAULT} />
|
||||
<TextInput
|
||||
value={newSubtask}
|
||||
onChangeText={setNewSubtask}
|
||||
onSubmitEditing={handleAddPendingSubtask}
|
||||
onFocus={() => setTimeout(() => scrollRef.current?.scrollToEnd({ animated: true }), 300)}
|
||||
placeholder={t('task.addSubtask')}
|
||||
placeholderTextColor={isDark ? '#A0A0A0' : '#6B6B6B'}
|
||||
className={`ml-2 flex-1 text-base ${isDark ? 'text-[#F5F5F5]' : 'text-[#1A1A1A]'}`}
|
||||
style={{ fontFamily: 'Inter_400Regular' }}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="h-32" />
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue