This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { Tabs } from 'expo-router';
|
||||
import React from 'react';
|
||||
import { TabBarIcon } from '@/components/TabBarIcon';
|
||||
import { useSettings } from '@/theme';
|
||||
|
||||
export default function TabLayout() {
|
||||
const { theme } = useSettings();
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
tabBarActiveTintColor: theme.accent,
|
||||
tabBarInactiveTintColor: '#8E8E8E',
|
||||
tabBarStyle: {
|
||||
backgroundColor: theme.tabBarBg,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: theme.border,
|
||||
height: 64,
|
||||
paddingBottom: 0,
|
||||
},
|
||||
tabBarLabelStyle: {
|
||||
fontSize: 11,
|
||||
fontWeight: '500',
|
||||
},
|
||||
headerShown: false,
|
||||
}}
|
||||
>
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: 'Tasks',
|
||||
tabBarIcon: ({ focused, color }) => (
|
||||
<TabBarIcon name="checklist" focused={focused} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="calendar"
|
||||
options={{
|
||||
title: 'Calendar',
|
||||
tabBarIcon: ({ focused, color }) => (
|
||||
<TabBarIcon name="calendar" focused={focused} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="settings"
|
||||
options={{
|
||||
title: 'Settings',
|
||||
tabBarIcon: ({ focused, color }) => (
|
||||
<TabBarIcon name="gear" focused={focused} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import { View, Text, StyleSheet, SafeAreaView, ScrollView, TouchableOpacity, FlatList } from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { Header } from '@/components/Header';
|
||||
import { useTasksByDate } from '@/hooks/useTasks';
|
||||
import { toggleTaskComplete } from '@/utils/taskActions';
|
||||
import { useSettings } from '@/theme';
|
||||
import { TaskItem } from '@/components/TaskItem';
|
||||
import { QuickAddBar } from '@/components/QuickAddBar';
|
||||
import { TaskData } from '@/types';
|
||||
import { format, startOfMonth, endOfMonth, eachDayOfInterval, isSameDay, isSameMonth, addMonths, isToday } from 'date-fns';
|
||||
import Svg, { Path } from 'react-native-svg';
|
||||
|
||||
const DAY_WIDTH = 44;
|
||||
const DAY_GAP = 6;
|
||||
|
||||
export default function CalendarScreen() {
|
||||
const router = useRouter();
|
||||
const { theme } = useSettings();
|
||||
const [visibleMonth, setVisibleMonth] = useState(() => new Date());
|
||||
const [selectedDate, setSelectedDate] = useState(() => new Date());
|
||||
const stripRef = useRef<ScrollView>(null);
|
||||
|
||||
const days = useMemo(
|
||||
() => eachDayOfInterval({ start: startOfMonth(visibleMonth), end: endOfMonth(visibleMonth) }),
|
||||
[visibleMonth]
|
||||
);
|
||||
|
||||
const { tasks, loading } = useTasksByDate(selectedDate);
|
||||
|
||||
const handleDayPress = (day: Date) => {
|
||||
setSelectedDate(day);
|
||||
if (!isSameMonth(day, visibleMonth)) {
|
||||
setVisibleMonth(day);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrevMonth = () => {
|
||||
const prev = addMonths(visibleMonth, -1);
|
||||
setVisibleMonth(prev);
|
||||
if (!isSameMonth(selectedDate, prev)) {
|
||||
setSelectedDate(startOfMonth(prev));
|
||||
}
|
||||
};
|
||||
|
||||
const handleNextMonth = () => {
|
||||
const next = addMonths(visibleMonth, 1);
|
||||
setVisibleMonth(next);
|
||||
if (!isSameMonth(selectedDate, next)) {
|
||||
setSelectedDate(startOfMonth(next));
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleComplete = async (taskId: string) => {
|
||||
await toggleTaskComplete(taskId);
|
||||
};
|
||||
|
||||
const today = new Date();
|
||||
|
||||
const scrollToDay = (day: Date) => {
|
||||
const index = days.findIndex((d) => isSameDay(d, day));
|
||||
if (index >= 0) {
|
||||
stripRef.current?.scrollTo({ x: Math.max(0, index * (DAY_WIDTH + DAY_GAP) - 24), animated: true });
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
scrollToDay(isSameMonth(selectedDate, visibleMonth) ? selectedDate : today);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [visibleMonth]);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
||||
<Header title="Calendar" showLogo={false} />
|
||||
|
||||
<ScrollView
|
||||
ref={stripRef}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.dateStrip}
|
||||
>
|
||||
{days.map((day) => {
|
||||
const isSelected = isSameDay(day, selectedDate);
|
||||
const isCurrent = isToday(day);
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={day.toISOString()}
|
||||
style={[
|
||||
styles.dayButton,
|
||||
{ backgroundColor: theme.card, borderColor: theme.borderStrong },
|
||||
isCurrent && !isSelected && { borderColor: theme.accent, borderWidth: 1.5 },
|
||||
isSelected && { backgroundColor: theme.accent, borderColor: theme.accent },
|
||||
]}
|
||||
onPress={() => handleDayPress(day)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[styles.dayWeekday, { color: theme.textMuted }, isSelected && styles.dayTextSelected]}>
|
||||
{format(day, 'EEE').charAt(0)}
|
||||
</Text>
|
||||
<Text style={[styles.dayNumber, { color: theme.text }, isSelected && styles.dayTextSelected]}>
|
||||
{format(day, 'd')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
|
||||
<View style={styles.monthRow}>
|
||||
<TouchableOpacity onPress={handlePrevMonth} style={[styles.monthNav, { borderColor: theme.borderStrong, backgroundColor: theme.card }]} activeOpacity={0.7}>
|
||||
<Svg width={20} height={20} viewBox="0 0 24 24">
|
||||
<Path d="M15 18l-6-6 6-6" stroke={theme.textFaint} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
</Svg>
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.monthLabel, { color: theme.text }]}>{format(visibleMonth, 'MMM yyyy').toUpperCase()}</Text>
|
||||
<TouchableOpacity onPress={handleNextMonth} style={[styles.monthNav, { borderColor: theme.borderStrong, backgroundColor: theme.card }]} activeOpacity={0.7}>
|
||||
<Svg width={20} height={20} viewBox="0 0 24 24">
|
||||
<Path d="M9 6l6 6-6 6" stroke={theme.textFaint} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
</Svg>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<FlatList
|
||||
data={tasks}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<TaskItem
|
||||
task={item as TaskData}
|
||||
onToggle={() => handleToggleComplete(item.id)}
|
||||
onPress={() => router.push({ pathname: '/task-detail', params: { id: item.id } })}
|
||||
/>
|
||||
)}
|
||||
ItemSeparatorComponent={() => <View style={styles.separator} />}
|
||||
ListEmptyComponent={
|
||||
loading ? (
|
||||
<View style={styles.emptyState}>
|
||||
<Text style={[styles.emptyText, { color: theme.textFaint }]}>Loading...</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.emptyState}>
|
||||
<Text style={[styles.emptyText, { color: theme.textSecondary }]}>No tasks scheduled.</Text>
|
||||
<Text style={[styles.emptySubtext, { color: theme.textMuted }]}>Use the bar below to add one</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
contentContainerStyle={styles.listContent}
|
||||
/>
|
||||
|
||||
<QuickAddBar
|
||||
dueDate={selectedDate.getTime()}
|
||||
placeholder={`Add task for ${format(selectedDate, 'MMM d')}`}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
dateStrip: {
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 12,
|
||||
gap: DAY_GAP,
|
||||
},
|
||||
dayButton: {
|
||||
width: DAY_WIDTH,
|
||||
height: 60,
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 2,
|
||||
},
|
||||
dayTextSelected: {
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
dayWeekday: {
|
||||
fontSize: 11,
|
||||
fontWeight: '600',
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
dayNumber: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
monthRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 24,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
monthNav: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
monthLabel: {
|
||||
fontSize: 15,
|
||||
fontWeight: '700',
|
||||
letterSpacing: 1,
|
||||
minWidth: 120,
|
||||
textAlign: 'center',
|
||||
},
|
||||
listContent: {
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 4,
|
||||
paddingBottom: 100,
|
||||
flexGrow: 1,
|
||||
},
|
||||
separator: {
|
||||
height: 8,
|
||||
},
|
||||
emptyState: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 64,
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
marginBottom: 4,
|
||||
},
|
||||
emptySubtext: {
|
||||
fontSize: 13,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet, SafeAreaView } from 'react-native';
|
||||
import { Header } from '@/components/Header';
|
||||
import { CategoryFilter } from '@/components/CategoryFilter';
|
||||
import { TaskList } from '@/components/TaskList';
|
||||
import { QuickAddBar } from '@/components/QuickAddBar';
|
||||
import { useDatabase } from '@/hooks/useDatabase';
|
||||
import { useSettings } from '@/theme';
|
||||
|
||||
export default function TasksScreen() {
|
||||
const { isReady } = useDatabase();
|
||||
const { theme } = useSettings();
|
||||
const [selectedCategory, setSelectedCategory] = React.useState<string>('all');
|
||||
const [selectionActive, setSelectionActive] = React.useState(false);
|
||||
|
||||
if (!isReady) {
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: theme.background }]}>
|
||||
<Text style={{ color: theme.textFaint }}>Loading...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
||||
<Header title="TODO" showLogo={true} />
|
||||
<CategoryFilter selected={selectedCategory} onSelect={setSelectedCategory} />
|
||||
<TaskList categoryId={selectedCategory} onSelectionChange={setSelectionActive} />
|
||||
<QuickAddBar />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,256 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, StyleSheet, SafeAreaView, ScrollView, Switch, TouchableOpacity } from 'react-native';
|
||||
import { Header } from '@/components/Header';
|
||||
import { ListItem } from '@/components/ListItem';
|
||||
import { OptionPickerModal } from '@/components/OptionPickerModal';
|
||||
import { CategoryEditorModal } from '@/components/CategoryEditorModal';
|
||||
import { SyncModal } from '@/components/SyncModal';
|
||||
import { FriendsModal } from '@/components/FriendsModal';
|
||||
import { LegalModal } from '@/components/LegalModal';
|
||||
import SyncStatus from '@/components/SyncStatus';
|
||||
import { useSettings, SORT_OPTIONS, REMINDER_OPTIONS } from '@/theme';
|
||||
import { useCategories } from '@/hooks/useDatabase';
|
||||
import { getAuthUser, getAuthToken } from '@/services/auth';
|
||||
import { getLastSyncTime } from '@/database/sync';
|
||||
import Category from '@/models/Category';
|
||||
import Svg, { Path } from 'react-native-svg';
|
||||
|
||||
export default function SettingsScreen() {
|
||||
const { theme, notifications, setNotifications, defaultCategoryId, setDefaultCategoryId, sortBy, setSortBy, reminderPreference, setReminderPreference } = useSettings();
|
||||
const categories = useCategories();
|
||||
const [picker, setPicker] = useState<null | 'category' | 'sort' | 'reminder'>(null);
|
||||
const [editingCategory, setEditingCategory] = useState<Category | null | 'new'>(null);
|
||||
const [syncVisible, setSyncVisible] = useState(false);
|
||||
const [friendsVisible, setFriendsVisible] = useState(false);
|
||||
const [legalVisible, setLegalVisible] = useState<null | 'privacy' | 'terms'>(null);
|
||||
const [syncSubtitle, setSyncSubtitle] = useState('Checking...');
|
||||
|
||||
const refreshSyncStatus = async () => {
|
||||
const token = await getAuthToken();
|
||||
if (!token) {
|
||||
setSyncSubtitle('Not signed in');
|
||||
return;
|
||||
}
|
||||
const user = await getAuthUser();
|
||||
const lastSync = await getLastSyncTime();
|
||||
if (user) {
|
||||
setSyncSubtitle(
|
||||
lastSync ? `Signed in · synced ${formatSyncTime(lastSync)}` : `Signed in as ${user.username}`
|
||||
);
|
||||
} else {
|
||||
setSyncSubtitle('Not signed in');
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
refreshSyncStatus();
|
||||
}, []);
|
||||
|
||||
const defaultCategory = categories.find((c) => c.id === defaultCategoryId);
|
||||
const defaultCategoryLabel = defaultCategory?.name ?? (categories.length > 0 ? categories[0].name : 'None');
|
||||
const reminderLabel = REMINDER_OPTIONS.find((o) => o.value === reminderPreference)?.label ?? 'No reminder';
|
||||
|
||||
const handleDefaultCategory = (value: string) => {
|
||||
setDefaultCategoryId(value);
|
||||
};
|
||||
|
||||
const handleReminderPreference = (value: string) => {
|
||||
setReminderPreference(value as typeof reminderPreference);
|
||||
};
|
||||
|
||||
const editorVisible = editingCategory !== null;
|
||||
const editorCategory = editingCategory === 'new' ? null : editingCategory;
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
||||
<Header title="Settings" showLogo={false} />
|
||||
<ScrollView contentContainerStyle={styles.content} showsVerticalScrollIndicator={false}>
|
||||
<Text style={[styles.sectionTitle, { color: theme.textMuted }]}>Categories</Text>
|
||||
{categories.map((category) => (
|
||||
<ListItem
|
||||
key={category.id}
|
||||
title={category.name}
|
||||
subtitle={category.id === defaultCategoryId ? 'Default category' : undefined}
|
||||
leftElement={
|
||||
<View style={[styles.categoryDot, { backgroundColor: category.color }]} />
|
||||
}
|
||||
onPress={() => setEditingCategory(category)}
|
||||
showChevron
|
||||
/>
|
||||
))}
|
||||
<TouchableOpacity
|
||||
style={[styles.addCategoryRow, { borderColor: theme.borderStrong }]}
|
||||
onPress={() => setEditingCategory('new')}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={[styles.addIcon, { backgroundColor: theme.accentSoft }]}>
|
||||
<Svg width={16} height={16} viewBox="0 0 24 24">
|
||||
<Path d="M12 5v14M5 12h14" stroke={theme.accent} strokeWidth={2} strokeLinecap="round" />
|
||||
</Svg>
|
||||
</View>
|
||||
<Text style={[styles.addCategoryText, { color: theme.accent }]}>Add Category</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.sectionTitle, { color: theme.textMuted }]}>Notifications</Text>
|
||||
<ListItem
|
||||
title="Push Notifications"
|
||||
subtitle="Receive task reminders"
|
||||
rightElement={
|
||||
<Switch
|
||||
value={notifications}
|
||||
onValueChange={setNotifications}
|
||||
thumbColor="#FFFFFF"
|
||||
trackColor={{ false: theme.borderStrong, true: theme.accent }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ListItem
|
||||
title="Reminder Preferences"
|
||||
subtitle={notifications ? reminderLabel : 'Enable notifications first'}
|
||||
onPress={() => notifications && setPicker('reminder')}
|
||||
showChevron
|
||||
/>
|
||||
<Text style={[styles.sectionTitle, { color: theme.textMuted }]}>Tasks</Text>
|
||||
<ListItem
|
||||
title="Default Category"
|
||||
subtitle={defaultCategoryLabel}
|
||||
onPress={() => setPicker('category')}
|
||||
showChevron
|
||||
/>
|
||||
<ListItem
|
||||
title="Sort Tasks By"
|
||||
subtitle={SORT_OPTIONS.find((o) => o.value === sortBy)?.label ?? 'Date'}
|
||||
onPress={() => setPicker('sort')}
|
||||
showChevron
|
||||
/>
|
||||
<Text style={[styles.sectionTitle, { color: theme.textMuted }]}>Data</Text>
|
||||
<SyncStatus />
|
||||
<ListItem
|
||||
title="Backup & Sync"
|
||||
subtitle={syncSubtitle}
|
||||
onPress={() => setSyncVisible(true)}
|
||||
showChevron
|
||||
/>
|
||||
<ListItem
|
||||
title="Friends"
|
||||
subtitle="Manage friends and requests"
|
||||
onPress={() => setFriendsVisible(true)}
|
||||
showChevron
|
||||
/>
|
||||
<Text style={[styles.sectionTitle, { color: theme.textMuted }]}>About</Text>
|
||||
<ListItem
|
||||
title="Version"
|
||||
subtitle="1.0.0"
|
||||
/>
|
||||
<ListItem
|
||||
title="Privacy Policy"
|
||||
onPress={() => setLegalVisible('privacy')}
|
||||
showChevron
|
||||
/>
|
||||
<ListItem
|
||||
title="Terms of Service"
|
||||
onPress={() => setLegalVisible('terms')}
|
||||
showChevron
|
||||
/>
|
||||
</ScrollView>
|
||||
|
||||
<OptionPickerModal
|
||||
visible={picker === 'category'}
|
||||
title="Default Category"
|
||||
options={categories.map((c) => ({ value: c.id, label: c.name, color: c.color }))}
|
||||
selectedValue={defaultCategoryId || categories[0]?.id}
|
||||
onSelect={handleDefaultCategory}
|
||||
onClose={() => setPicker(null)}
|
||||
/>
|
||||
|
||||
<OptionPickerModal
|
||||
visible={picker === 'sort'}
|
||||
title="Sort Tasks By"
|
||||
options={SORT_OPTIONS}
|
||||
selectedValue={sortBy}
|
||||
onSelect={(value) => setSortBy(value as typeof sortBy)}
|
||||
onClose={() => setPicker(null)}
|
||||
/>
|
||||
|
||||
<OptionPickerModal
|
||||
visible={picker === 'reminder'}
|
||||
title="Reminder Preferences"
|
||||
options={REMINDER_OPTIONS}
|
||||
selectedValue={reminderPreference}
|
||||
onSelect={handleReminderPreference}
|
||||
onClose={() => setPicker(null)}
|
||||
/>
|
||||
|
||||
<CategoryEditorModal
|
||||
visible={editorVisible}
|
||||
category={editorCategory}
|
||||
categoryCount={categories.length}
|
||||
onClose={() => setEditingCategory(null)}
|
||||
/>
|
||||
|
||||
<SyncModal visible={syncVisible} onClose={() => { setSyncVisible(false); refreshSyncStatus(); }} />
|
||||
|
||||
<FriendsModal visible={friendsVisible} onClose={() => setFriendsVisible(false)} />
|
||||
|
||||
<LegalModal
|
||||
visible={legalVisible !== null}
|
||||
type={legalVisible}
|
||||
onClose={() => setLegalVisible(null)}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
function formatSyncTime(timestamp: number): string {
|
||||
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
||||
if (seconds < 60) return 'just now';
|
||||
if (seconds < 3600) return `${Math.floor(seconds / 60)} min ago`;
|
||||
if (seconds < 86400) return `${Math.floor(seconds / 3600)} h ago`;
|
||||
return new Date(timestamp).toLocaleDateString();
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 8,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
marginTop: 24,
|
||||
marginBottom: 8,
|
||||
marginLeft: 4,
|
||||
},
|
||||
categoryDot: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
},
|
||||
addCategoryRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderStyle: 'dashed',
|
||||
marginBottom: 8,
|
||||
},
|
||||
addIcon: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
addCategoryText: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Stack } from 'expo-router';
|
||||
import { DatabaseProvider } from '@/hooks/useDatabase';
|
||||
import { SettingsProvider } from '@/theme';
|
||||
import { FriendsProvider } from '@/hooks/useFriends';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
|
||||
function RootNavigator() {
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<StatusBar style="light" />
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name="(tabs)" />
|
||||
<Stack.Screen name="add-task" />
|
||||
<Stack.Screen name="task-detail" />
|
||||
</Stack>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<SettingsProvider>
|
||||
<DatabaseProvider>
|
||||
<FriendsProvider>
|
||||
<RootNavigator />
|
||||
</FriendsProvider>
|
||||
</DatabaseProvider>
|
||||
</SettingsProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { View, Text, StyleSheet, SafeAreaView, ScrollView, KeyboardAvoidingView, Platform } from 'react-native';
|
||||
import { Header } from '@/components/Header';
|
||||
import { CategorySelector } from '@/components/CategorySelector';
|
||||
import { TaskNameInput } from '@/components/TaskNameInput';
|
||||
import { SubtasksSection } from '@/components/SubtasksSection';
|
||||
import { DateTimePickerComponent } from '@/components/DateTimePicker';
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import { PrioritySelector } from '@/components/PrioritySelector';
|
||||
import { RepeatSelector } from '@/components/RepeatSelector';
|
||||
import { ReminderSelector } from '@/components/ReminderSelector';
|
||||
import { DescriptionInput } from '@/components/DescriptionInput';
|
||||
import { FormButtons } from '@/components/FormButtons';
|
||||
import { AssigneeSelector } from '@/components/AssigneeSelector';
|
||||
import { useForm, FormProvider, Controller } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { useDatabase, useCategories } from '@/hooks/useDatabase';
|
||||
import { database, collections } from '@/database';
|
||||
import { TaskFormData } from '@/types';
|
||||
import { useSettings } from '@/theme';
|
||||
import { scheduleTaskReminder } from '@/services/notifications';
|
||||
import { useFriends } from '@/hooks/useFriends';
|
||||
|
||||
const taskSchema = z.object({
|
||||
title: z.string().trim().min(1, 'Task name is required').max(100),
|
||||
description: z.string().max(1000).optional(),
|
||||
categoryId: z.string().min(1, 'Category is required'),
|
||||
priority: z.enum(['none', 'low', 'medium', 'high', 'critical']),
|
||||
dueDate: z.date().nullable().optional(),
|
||||
dueTime: z.string().optional(),
|
||||
endTime: z.string().optional(),
|
||||
allDay: z.boolean().optional(),
|
||||
repeat: z.enum(['none', 'daily', 'weekly', 'monthly', 'custom']),
|
||||
repeatInterval: z.number().int().min(1).max(30).optional(),
|
||||
repeatDays: z.array(z.number().int().min(0).max(6)).optional(),
|
||||
reminder: z.enum(['none', 'at_time', '15', '30', '60', '120', '1440']),
|
||||
assigneeId: z.string().nullable().optional(),
|
||||
subtasks: z.array(z.object({ title: z.string(), _key: z.string().optional() })).optional(),
|
||||
});
|
||||
|
||||
export default function AddTaskScreen() {
|
||||
const { isReady } = useDatabase();
|
||||
const categories = useCategories();
|
||||
const { defaultCategoryId } = useSettings();
|
||||
const router = useRouter();
|
||||
const { theme } = useSettings();
|
||||
const { date: dateParam } = useLocalSearchParams<{ date?: string }>();
|
||||
const { friends } = useFriends();
|
||||
const initialCategory = defaultCategoryId || categories[0]?.id || '';
|
||||
const initialDate = useMemo(() => {
|
||||
if (!dateParam) return null;
|
||||
const parsed = new Date(Array.isArray(dateParam) ? dateParam[0] : dateParam);
|
||||
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
||||
}, [dateParam]);
|
||||
|
||||
const methods = useForm<TaskFormData>({
|
||||
resolver: zodResolver(taskSchema),
|
||||
defaultValues: {
|
||||
title: '',
|
||||
description: '',
|
||||
categoryId: initialCategory,
|
||||
priority: 'none',
|
||||
dueDate: initialDate,
|
||||
dueTime: '',
|
||||
endTime: '',
|
||||
allDay: false,
|
||||
repeat: 'none',
|
||||
repeatInterval: 1,
|
||||
repeatDays: [],
|
||||
reminder: 'none',
|
||||
assigneeId: null,
|
||||
subtasks: [],
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
watch,
|
||||
setValue,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = methods;
|
||||
|
||||
const categoryId = watch('categoryId');
|
||||
const priority = watch('priority');
|
||||
const repeat = watch('repeat');
|
||||
const repeatInterval = watch('repeatInterval') ?? 1;
|
||||
const repeatDays = watch('repeatDays') ?? [];
|
||||
const reminder = watch('reminder');
|
||||
const dueDate = watch('dueDate');
|
||||
const assigneeId = watch('assigneeId');
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!categoryId && initialCategory) {
|
||||
setValue('categoryId', initialCategory);
|
||||
}
|
||||
}, [initialCategory, categoryId, setValue]);
|
||||
|
||||
const onSubmit = async (data: TaskFormData) => {
|
||||
if (!isReady) return;
|
||||
|
||||
const now = new Date();
|
||||
const dueDateTimestamp = data.dueDate ? data.dueDate.getTime() : 0;
|
||||
const seriesId = data.repeat !== 'none'
|
||||
? `series_${Math.random().toString(36).slice(2, 10)}${Date.now().toString(36)}`
|
||||
: '';
|
||||
|
||||
let createdTask: any = null;
|
||||
|
||||
await database.write(async () => {
|
||||
const task = await collections.tasks.create((t) => {
|
||||
t.title = data.title.trim();
|
||||
t.description = data.description || '';
|
||||
t.categoryId = data.categoryId;
|
||||
t.priority = data.priority;
|
||||
t.completed = false;
|
||||
t.dueDate = dueDateTimestamp;
|
||||
t.dueTime = data.dueTime || '';
|
||||
t.endTime = data.endTime || '';
|
||||
t.allDay = data.allDay ?? false;
|
||||
t.repeat = data.repeat;
|
||||
t.repeatInterval = data.repeatInterval || 1;
|
||||
t.repeatDays = (data.repeatDays || []).join(',');
|
||||
t.seriesId = seriesId;
|
||||
t.reminder = data.reminder || 'none';
|
||||
t.assigneeId = data.assigneeId ?? null;
|
||||
t.createdAt = now;
|
||||
t.updatedAt = now;
|
||||
});
|
||||
createdTask = task;
|
||||
if (data.subtasks && data.subtasks.length > 0) {
|
||||
for (let i = 0; i < data.subtasks.length; i++) {
|
||||
const subtask = data.subtasks[i];
|
||||
if (subtask.title.trim()) {
|
||||
await collections.subtasks.create((s) => {
|
||||
s.taskId = task.id;
|
||||
s.title = subtask.title.trim();
|
||||
s.description = '';
|
||||
s.priority = 'none';
|
||||
s.completed = false;
|
||||
s.dueDate = 0;
|
||||
s.dueTime = '';
|
||||
s.endTime = '';
|
||||
s.reminder = 'none';
|
||||
s.assigneeId = null;
|
||||
s.order = i;
|
||||
s.createdAt = now;
|
||||
s.updatedAt = now;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await scheduleTaskReminder(createdTask);
|
||||
|
||||
router.back();
|
||||
};
|
||||
|
||||
if (!isReady) {
|
||||
return (
|
||||
<View style={[styles.loadingContainer, { backgroundColor: theme.background }]}>
|
||||
<Text style={{ color: theme.textFaint }}>Loading...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
||||
<Header title="Add New Task" showLogo={true} />
|
||||
<FormProvider {...methods}>
|
||||
<KeyboardAvoidingView
|
||||
style={styles.keyboardAvoiding}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<CategorySelector
|
||||
value={categoryId}
|
||||
onChange={(value) => setValue('categoryId', value)}
|
||||
error={errors.categoryId?.message}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<TaskNameInput
|
||||
value={field.value}
|
||||
onChangeText={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
error={errors.title?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<SubtasksSection control={control} />
|
||||
<PrioritySelector
|
||||
value={priority}
|
||||
onChange={(value) => setValue('priority', value)}
|
||||
/>
|
||||
<DateTimePickerComponent control={control} />
|
||||
<RepeatSelector
|
||||
value={repeat}
|
||||
interval={repeatInterval}
|
||||
days={repeatDays}
|
||||
onChange={(nextRepeat, nextInterval, nextDays) => {
|
||||
setValue('repeat', nextRepeat);
|
||||
setValue('repeatInterval', nextInterval);
|
||||
setValue('repeatDays', nextDays);
|
||||
}}
|
||||
/>
|
||||
<ReminderSelector
|
||||
value={reminder}
|
||||
hasDueDate={!!dueDate}
|
||||
onChange={(value) => setValue('reminder', value)}
|
||||
/>
|
||||
<AssigneeSelector
|
||||
value={assigneeId}
|
||||
onChange={(value: string | null) => setValue('assigneeId', value)}
|
||||
friends={friends.map((f) => f.username)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<DescriptionInput
|
||||
value={field.value ?? ''}
|
||||
onChangeText={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
<FormButtons onSubmit={handleSubmit(onSubmit)} submitLabel="Save" />
|
||||
</FormProvider>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
keyboardAvoiding: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 100,
|
||||
gap: 24,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,331 @@
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet, SafeAreaView, ScrollView, KeyboardAvoidingView, Platform, TouchableOpacity } from 'react-native';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { Header } from '@/components/Header';
|
||||
import { CategorySelector } from '@/components/CategorySelector';
|
||||
import { TaskNameInput } from '@/components/TaskNameInput';
|
||||
import { SubtasksSection } from '@/components/SubtasksSection';
|
||||
import { DateTimePickerComponent } from '@/components/DateTimePicker';
|
||||
import { PrioritySelector } from '@/components/PrioritySelector';
|
||||
import { RepeatSelector } from '@/components/RepeatSelector';
|
||||
import { ReminderSelector } from '@/components/ReminderSelector';
|
||||
import { DescriptionInput } from '@/components/DescriptionInput';
|
||||
import { FormButtons } from '@/components/FormButtons';
|
||||
import { TaskDeleteModal } from '@/components/TaskDeleteModal';
|
||||
import { AssigneeSelector } from '@/components/AssigneeSelector';
|
||||
import { useForm, FormProvider, Controller } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { useDatabase } from '@/hooks/useDatabase';
|
||||
import { database, collections } from '@/database';
|
||||
import { Q } from '@nozbe/watermelondb';
|
||||
import { TaskFormData } from '@/types';
|
||||
import { useSettings } from '@/theme';
|
||||
import { deleteTaskOccurrences } from '@/utils/taskActions';
|
||||
import { scheduleTaskReminder, cancelTaskReminder } from '@/services/notifications';
|
||||
import { useFriends } from '@/hooks/useFriends';
|
||||
import Svg, { Path } from 'react-native-svg';
|
||||
|
||||
const taskSchema = z.object({
|
||||
title: z.string().trim().min(1, 'Task name is required').max(100),
|
||||
description: z.string().max(1000).optional(),
|
||||
categoryId: z.string().min(1, 'Category is required'),
|
||||
priority: z.enum(['none', 'low', 'medium', 'high', 'critical']),
|
||||
dueDate: z.date().nullable().optional(),
|
||||
dueTime: z.string().optional(),
|
||||
endTime: z.string().optional(),
|
||||
allDay: z.boolean().optional(),
|
||||
repeat: z.enum(['none', 'daily', 'weekly', 'monthly', 'custom']),
|
||||
repeatInterval: z.number().int().min(1).max(30).optional(),
|
||||
repeatDays: z.array(z.number().int().min(0).max(6)).optional(),
|
||||
reminder: z.enum(['none', 'at_time', '15', '30', '60', '120', '1440']),
|
||||
assigneeId: z.string().nullable().optional(),
|
||||
subtasks: z.array(z.object({ title: z.string(), _key: z.string().optional() })).optional(),
|
||||
});
|
||||
|
||||
export default function TaskDetailScreen() {
|
||||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
const { isReady } = useDatabase();
|
||||
const router = useRouter();
|
||||
const { theme } = useSettings();
|
||||
const { friends } = useFriends();
|
||||
const [loaded, setLoaded] = React.useState(false);
|
||||
const [notFound, setNotFound] = React.useState(false);
|
||||
|
||||
const methods = useForm<TaskFormData>({
|
||||
resolver: zodResolver(taskSchema),
|
||||
defaultValues: {
|
||||
title: '',
|
||||
description: '',
|
||||
categoryId: '',
|
||||
priority: 'none',
|
||||
dueDate: null,
|
||||
dueTime: '',
|
||||
endTime: '',
|
||||
allDay: false,
|
||||
repeat: 'none',
|
||||
repeatInterval: 1,
|
||||
repeatDays: [],
|
||||
reminder: 'none',
|
||||
assigneeId: null,
|
||||
subtasks: [],
|
||||
},
|
||||
});
|
||||
|
||||
const { handleSubmit, watch, setValue, control, reset, formState: { errors } } = methods;
|
||||
|
||||
const categoryId = watch('categoryId');
|
||||
const priority = watch('priority');
|
||||
const repeat = watch('repeat');
|
||||
const repeatInterval = watch('repeatInterval') ?? 1;
|
||||
const repeatDays = watch('repeatDays') ?? [];
|
||||
const reminder = watch('reminder');
|
||||
const dueDate = watch('dueDate');
|
||||
const assigneeId = watch('assigneeId');
|
||||
const [deleteModalVisible, setDeleteModalVisible] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!id || !isReady) return;
|
||||
let mounted = true;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const task = await collections.tasks.find(id);
|
||||
const subtasks = await collections.subtasks.query(Q.where('task_id', id)).fetch();
|
||||
|
||||
if (!mounted) return;
|
||||
reset({
|
||||
title: task.title,
|
||||
description: task.description,
|
||||
categoryId: task.categoryId,
|
||||
priority: task.priority,
|
||||
dueDate: task.dueDate ? new Date(task.dueDate) : null,
|
||||
dueTime: task.dueTime,
|
||||
endTime: task.endTime || '',
|
||||
allDay: task.allDay ?? false,
|
||||
repeat: task.repeat,
|
||||
repeatInterval: task.repeatInterval || 1,
|
||||
repeatDays: (task.repeatDays || '').split(',').map(Number).filter((d) => !Number.isNaN(d)),
|
||||
reminder: (task.reminder || 'none') as TaskFormData['reminder'],
|
||||
assigneeId: task.assigneeId ?? null,
|
||||
subtasks: subtasks.map((s) => ({ title: s.title, _key: s.id })),
|
||||
});
|
||||
setLoaded(true);
|
||||
} catch {
|
||||
if (mounted) setNotFound(true);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => { mounted = false; };
|
||||
}, [id, isReady, reset]);
|
||||
|
||||
const onSubmit = async (data: TaskFormData) => {
|
||||
if (!id || !isReady) return;
|
||||
|
||||
const now = new Date();
|
||||
const dueDateTimestamp = data.dueDate ? data.dueDate.getTime() : 0;
|
||||
|
||||
let savedTask: any = null;
|
||||
|
||||
await database.write(async () => {
|
||||
const task = await collections.tasks.find(id);
|
||||
savedTask = task;
|
||||
const existingSubtasks = await collections.subtasks.query(Q.where('task_id', id)).fetch();
|
||||
for (const subtask of existingSubtasks) {
|
||||
await subtask.destroyPermanently();
|
||||
}
|
||||
|
||||
await task.update((t) => {
|
||||
t.title = data.title.trim();
|
||||
t.description = data.description || '';
|
||||
t.categoryId = data.categoryId;
|
||||
t.priority = data.priority;
|
||||
t.dueDate = dueDateTimestamp;
|
||||
t.dueTime = data.dueTime || '';
|
||||
t.endTime = data.endTime || '';
|
||||
t.allDay = data.allDay ?? false;
|
||||
t.repeat = data.repeat;
|
||||
t.repeatInterval = data.repeatInterval || 1;
|
||||
t.repeatDays = (data.repeatDays || []).join(',');
|
||||
t.reminder = data.reminder || 'none';
|
||||
t.assigneeId = data.assigneeId ?? null;
|
||||
if (data.repeat !== 'none' && !t.seriesId) {
|
||||
t.seriesId = `series_${Math.random().toString(36).slice(2, 10)}${Date.now().toString(36)}`;
|
||||
}
|
||||
t.updatedAt = now;
|
||||
});
|
||||
|
||||
if (data.subtasks && data.subtasks.length > 0) {
|
||||
for (let i = 0; i < data.subtasks.length; i++) {
|
||||
const subtask = data.subtasks[i];
|
||||
if (subtask.title.trim()) {
|
||||
await collections.subtasks.create((s) => {
|
||||
s.taskId = task.id;
|
||||
s.title = subtask.title.trim();
|
||||
s.description = '';
|
||||
s.priority = 'none';
|
||||
s.completed = false;
|
||||
s.dueDate = 0;
|
||||
s.dueTime = '';
|
||||
s.endTime = '';
|
||||
s.allDay = false;
|
||||
s.repeat = 'none';
|
||||
s.repeatInterval = 1;
|
||||
s.repeatDays = '';
|
||||
s.seriesId = '';
|
||||
s.reminder = 'none';
|
||||
s.assigneeId = null;
|
||||
s.order = i;
|
||||
s.createdAt = now;
|
||||
s.updatedAt = now;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await scheduleTaskReminder(savedTask);
|
||||
|
||||
router.back();
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
setDeleteModalVisible(true);
|
||||
};
|
||||
|
||||
const doDelete = async (scope: 'this' | 'future' | 'all') => {
|
||||
if (!id) return;
|
||||
setDeleteModalVisible(false);
|
||||
await deleteTaskOccurrences(id, scope);
|
||||
router.back();
|
||||
};
|
||||
|
||||
if (!isReady || !loaded) {
|
||||
return (
|
||||
<View style={[styles.loadingContainer, { backgroundColor: theme.background }]}>
|
||||
<Text style={{ color: theme.textFaint }}>{notFound ? 'Task not found' : 'Loading...'}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
||||
<Header
|
||||
title="Edit Task"
|
||||
showLogo={true}
|
||||
rightAction={
|
||||
<TouchableOpacity onPress={handleDelete} activeOpacity={0.7} style={[styles.deleteButton, { borderColor: theme.accentBorder, backgroundColor: theme.accentSoft }]}>
|
||||
<Svg width={22} height={22} viewBox="0 0 24 24">
|
||||
<Path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13" stroke={theme.accent} strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
</Svg>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
/>
|
||||
<FormProvider {...methods}>
|
||||
<KeyboardAvoidingView
|
||||
style={styles.keyboardAvoiding}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<CategorySelector
|
||||
value={categoryId}
|
||||
onChange={(value) => setValue('categoryId', value)}
|
||||
error={errors.categoryId?.message}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<TaskNameInput
|
||||
value={field.value}
|
||||
onChangeText={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
error={errors.title?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<SubtasksSection control={control} />
|
||||
<PrioritySelector
|
||||
value={priority}
|
||||
onChange={(value) => setValue('priority', value)}
|
||||
/>
|
||||
<DateTimePickerComponent control={control} />
|
||||
<RepeatSelector
|
||||
value={repeat}
|
||||
interval={repeatInterval}
|
||||
days={repeatDays}
|
||||
onChange={(nextRepeat, nextInterval, nextDays) => {
|
||||
setValue('repeat', nextRepeat);
|
||||
setValue('repeatInterval', nextInterval);
|
||||
setValue('repeatDays', nextDays);
|
||||
}}
|
||||
/>
|
||||
<ReminderSelector
|
||||
value={reminder}
|
||||
hasDueDate={!!dueDate}
|
||||
onChange={(value) => setValue('reminder', value)}
|
||||
/>
|
||||
<AssigneeSelector
|
||||
value={assigneeId}
|
||||
onChange={(value: string | null) => setValue('assigneeId', value)}
|
||||
friends={friends.map((f) => f.username)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<DescriptionInput
|
||||
value={field.value ?? ''}
|
||||
onChangeText={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
<FormButtons onSubmit={handleSubmit(onSubmit)} submitLabel="Save" />
|
||||
</FormProvider>
|
||||
|
||||
<TaskDeleteModal
|
||||
visible={deleteModalVisible}
|
||||
taskId={id}
|
||||
taskTitle={methods.getValues('title')}
|
||||
isRepeating={repeat !== 'none'}
|
||||
onClose={() => setDeleteModalVisible(false)}
|
||||
onDelete={doDelete}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
keyboardAvoiding: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 100,
|
||||
gap: 24,
|
||||
},
|
||||
deleteButton: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user