diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 0d952d7..b046434 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -80,7 +80,7 @@ jobs: - name: Upload APK id: upload-apk - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 with: name: carry-your-live-release path: carry-your-live/android/app/build/outputs/release/app-release.apk @@ -95,7 +95,7 @@ jobs: fetch-depth: 0 - name: Download APK artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v3 with: name: carry-your-live-release path: . diff --git a/carry-your-live/app.json b/carry-your-live/app.json index b8302e7..07709b1 100644 --- a/carry-your-live/app.json +++ b/carry-your-live/app.json @@ -34,7 +34,9 @@ "color": "#1E88E5" } ], - "./plugins/withQuickAddWidget" + "./plugins/withQuickAddWidget", + "expo-router", + "expo-sqlite" ], "extra": { "eas": { diff --git a/carry-your-live/app/(tabs)/_layout.tsx b/carry-your-live/app/(tabs)/_layout.tsx index dc27529..87cca0d 100644 --- a/carry-your-live/app/(tabs)/_layout.tsx +++ b/carry-your-live/app/(tabs)/_layout.tsx @@ -1,7 +1,7 @@ import { Tabs } from 'expo-router'; import React from 'react'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { View, StyleSheet, TouchableOpacity, Animated, } from 'react-native'; +import { View, StyleSheet, TouchableOpacity, Animated } from 'react-native'; import { TabBarIcon } from '@/components/TabBarIcon'; import { useSettings } from '@/theme'; import { setLastVisitedTab } from '@/utils/tabHistory'; @@ -12,7 +12,7 @@ export default function TabLayout() { return ( { + tabBar={props => { return ( - {props.navigation.getState().routes.map((route) => { - const focused = props.navigation.getState().index === props.navigation.getState().routes.indexOf(route); + style={[ + styles.tabBarContainer, + { + backgroundColor: theme.tabBarBg, + paddingBottom: Math.max(props.insets.bottom, 8), + shadowColor: '#000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.1, + shadowRadius: 8, + elevation: 4, + }, + ]} + > + {props.navigation.getState().routes.map(route => { + const focused = + props.navigation.getState().index === + props.navigation.getState().routes.indexOf(route); return ( props.navigation.navigate(route.name as never)} accessibilityRole="button" - accessibilityLabel={route.name === 'index' ? 'ToDo' : route.name.charAt(0).toUpperCase() + route.name.slice(1)} + accessibilityLabel={ + route.name === 'index' + ? 'ToDo' + : route.name.charAt(0).toUpperCase() + route.name.slice(1) + } accessibilityState={{ selected: focused }} > - - - + + + ); })} @@ -129,4 +138,4 @@ const styles = StyleSheet.create({ justifyContent: 'center', position: 'relative', }, -}); \ No newline at end of file +}); diff --git a/carry-your-live/app/(tabs)/calendar.tsx b/carry-your-live/app/(tabs)/calendar.tsx index 59a9fca..b9bbc22 100644 --- a/carry-your-live/app/(tabs)/calendar.tsx +++ b/carry-your-live/app/(tabs)/calendar.tsx @@ -1,5 +1,15 @@ import React, { useMemo, useRef, useState, useCallback, useEffect } from 'react'; -import { View, Text, StyleSheet, SafeAreaView, ScrollView, TouchableOpacity, Animated, Dimensions, KeyboardAvoidingView } from 'react-native'; +import { + View, + Text, + StyleSheet, + SafeAreaView, + ScrollView, + TouchableOpacity, + Animated, + Dimensions, + KeyboardAvoidingView, +} from 'react-native'; import { useRouter, useFocusEffect } from 'expo-router'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import { Header } from '@/components/Header'; @@ -15,7 +25,15 @@ import { OptionPickerModal } from '@/components/OptionPickerModal'; import { TaskItem } from '@/components/TaskItem'; import { SubtaskItem } from '@/components/SubtaskItem'; import Task from '@/models/Task'; -import { format, addMonths, addDays, startOfMonth, isSameDay, isSameMonth, isToday } from 'date-fns'; +import { + format, + addMonths, + addDays, + startOfMonth, + isSameDay, + isSameMonth, + isToday, +} from 'date-fns'; import Svg, { Path } from 'react-native-svg'; import type { ThemeColors } from '@/theme'; import type { TaskData, SubtaskData } from '@/types'; @@ -24,8 +42,18 @@ const WIDTH = Dimensions.get('window').width; const GERMAN_WEEKDAYS = ['mo', 'di', 'mi', 'do', 'fr', 'sa', 'so']; const PRIORITY_RANK: Record = { none: 0, low: 1, medium: 2, high: 3, critical: 4 }; const MONTH_NAMES = [ - 'January', 'February', 'March', 'April', 'May', 'June', - 'July', 'August', 'September', 'October', 'November', 'December', + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', ]; export default function CalendarScreen() { @@ -70,7 +98,9 @@ export default function CalendarScreen() { const sorted = [...selectedDayTasks]; switch (sortBy) { case 'date': - sorted.sort((a, b) => (a.dueDate || Number.MAX_SAFE_INTEGER) - (b.dueDate || Number.MAX_SAFE_INTEGER)); + sorted.sort( + (a, b) => (a.dueDate || Number.MAX_SAFE_INTEGER) - (b.dueDate || Number.MAX_SAFE_INTEGER) + ); break; case 'priority': sorted.sort((a, b) => PRIORITY_RANK[b.priority] - PRIORITY_RANK[a.priority]); @@ -118,10 +148,18 @@ export default function CalendarScreen() { animatingRef.current = true; const w = gridWidthRef.current || WIDTH; const target = dir === 1 ? -w : w; - Animated.timing(translateX.current, { toValue: target, duration: 220, useNativeDriver: false }).start(() => { + Animated.timing(translateX.current, { + toValue: target, + duration: 220, + useNativeDriver: false, + }).start(() => { setVisibleMonth(next); translateX.current.setValue(-target); - Animated.timing(translateX.current, { toValue: 0, duration: 180, useNativeDriver: false }).start(() => { + Animated.timing(translateX.current, { + toValue: 0, + duration: 180, + useNativeDriver: false, + }).start(() => { animatingRef.current = false; }); }); @@ -148,11 +186,11 @@ export default function CalendarScreen() { .minDistance(6) .runOnJS(true) // eslint-disable-next-line react-hooks/refs - .onUpdate((e) => { + .onUpdate(e => { if (!animatingRef.current) translateX.current.setValue(e.translationX); }) // eslint-disable-next-line react-hooks/refs - .onEnd((e) => { + .onEnd(e => { if (animatingRef.current) return; const w = gridWidthRef.current || WIDTH; const dx = e.translationX; @@ -183,22 +221,26 @@ export default function CalendarScreen() { const monthOptions = MONTH_NAMES.map((label, i) => ({ value: String(i), label })); const yearOptions = useMemo(() => { const options: { value: string; label: string }[] = []; - for (let y = currentYear - 20; y <= currentYear + 10; y++) options.push({ value: String(y), label: String(y) }); + for (let y = currentYear - 20; y <= currentYear + 10; y++) + options.push({ value: String(y), label: String(y) }); return options; }, [currentYear]); - const handleToggleComplete = useCallback(async (taskId: string) => { - await confirmAndToggleTaskComplete(taskId, () => { - refreshDayTasks(); - refreshMonthTasks(); - refreshSubtasks(); - }); - }, [refreshDayTasks, refreshMonthTasks, refreshSubtasks]); + const handleToggleComplete = useCallback( + async (taskId: string) => { + await confirmAndToggleTaskComplete(taskId, () => { + refreshDayTasks(); + refreshMonthTasks(); + refreshSubtasks(); + }); + }, + [refreshDayTasks, refreshMonthTasks, refreshSubtasks] + ); const [expandedTasks, setExpandedTasks] = useState>(new Set()); const toggleExpand = useCallback((taskId: string) => { - setExpandedTasks((prev) => { + setExpandedTasks(prev => { const next = new Set(prev); if (next.has(taskId)) { next.delete(taskId); @@ -209,12 +251,15 @@ export default function CalendarScreen() { }); }, []); - const handleSubtaskToggle = useCallback(async (subtaskId: string) => { - await toggleSubtaskComplete(subtaskId); - refreshDayTasks(); - refreshMonthTasks(); - refreshSubtasks(); - }, [refreshDayTasks, refreshMonthTasks, refreshSubtasks]); + const handleSubtaskToggle = useCallback( + async (subtaskId: string) => { + await toggleSubtaskComplete(subtaskId); + refreshDayTasks(); + refreshMonthTasks(); + refreshSubtasks(); + }, + [refreshDayTasks, refreshMonthTasks, refreshSubtasks] + ); return ( @@ -229,53 +274,103 @@ export default function CalendarScreen() { accessibilityLabel="Back" > - + } /> - - - { - gridWidthRef.current = e.nativeEvent.layout.width; - }} - // eslint-disable-next-line react-hooks/refs - style={[styles.calendarArea, { transform: [{ translateX: translateX.current }] }]} - > + + + { + gridWidthRef.current = e.nativeEvent.layout.width; + }} + // eslint-disable-next-line react-hooks/refs + style={[styles.calendarArea, { transform: [{ translateX: translateX.current }] }]} + > transitionTo(-1)} - style={[styles.monthNav, { borderColor: theme.borderStrong, backgroundColor: theme.card }]} + style={[ + styles.monthNav, + { borderColor: theme.borderStrong, backgroundColor: theme.card }, + ]} activeOpacity={0.7} > - + - setMonthPickerVisible(true)} activeOpacity={0.7} style={styles.monthButton}> - {format(visibleMonth, 'MMMM')} + setMonthPickerVisible(true)} + activeOpacity={0.7} + style={styles.monthButton} + > + + {format(visibleMonth, 'MMMM')} + - + - setYearPickerVisible(true)} activeOpacity={0.7} style={styles.yearButton}> - {format(visibleMonth, 'yyyy')} + setYearPickerVisible(true)} + activeOpacity={0.7} + style={styles.yearButton} + > + + {format(visibleMonth, 'yyyy')} + transitionTo(1)} - style={[styles.monthNav, { borderColor: theme.borderStrong, backgroundColor: theme.card }]} + style={[ + styles.monthNav, + { borderColor: theme.borderStrong, backgroundColor: theme.card }, + ]} activeOpacity={0.7} > - + @@ -290,7 +385,7 @@ export default function CalendarScreen() { {weeks.map((week, wi) => ( - {week.map((day) => ( + {week.map(day => ( ))} - + - - {format(selectedDate, 'EEEE, MMMM d')} - {sortedDayTasks.length > 0 && ( - - {sortedDayTasks.length} event{sortedDayTasks.length === 1 ? '' : 's'} - - )} - - - {sortedDayTasks.length === 0 ? ( - - No events on this day - Tap a date or use the bar below - - ) : ( - sortedDayTasks.map((task) => { - const isExpanded = expandedTasks.has(task.id); - const itemSubtasks = subtasksByTask.get(task.id) ?? []; - return ( - - handleToggleComplete(task.id)} - onPress={(id) => - expandedTasks.has(id) - ? router.push({ pathname: '/task-detail', params: { id } }) - : toggleExpand(id) - } - onMenuOpen={(id) => openTaskEdit(id)} - onSubtaskToggle={handleSubtaskToggle} - /> - - ); - }) + + + {format(selectedDate, 'EEEE, MMMM d')} + + {sortedDayTasks.length > 0 && ( + + {sortedDayTasks.length} event{sortedDayTasks.length === 1 ? '' : 's'} + )} - + - + {sortedDayTasks.length === 0 ? ( + + + No events on this day + + + Tap a date or use the bar below + + + ) : ( + sortedDayTasks.map(task => { + const isExpanded = expandedTasks.has(task.id); + const itemSubtasks = subtasksByTask.get(task.id) ?? []; + return ( + + handleToggleComplete(task.id)} + onPress={id => + expandedTasks.has(id) + ? router.push({ pathname: '/task-detail', params: { id } }) + : toggleExpand(id) + } + onMenuOpen={id => openTaskEdit(id)} + onSubtaskToggle={handleSubtaskToggle} + /> + + ); + }) + )} + + + {modals(() => {})} @@ -419,7 +520,7 @@ const CalendarTaskRow = React.memo(function CalendarTaskRow({ /> {expanded && sortedSubtasks.length > 0 && ( - {sortedSubtasks.map((sub) => ( + {sortedSubtasks.map(sub => ( void; } -const DayCell = React.memo(function DayCell({ day, label, dotColor, selected, today, inMonth, theme, onPress }: DayCellProps) { +const DayCell = React.memo(function DayCell({ + day, + label, + dotColor, + selected, + today, + inMonth, + theme, + onPress, +}: DayCellProps) { const styles = React.useMemo(() => createStyles(theme), [theme]); return ( onPress(day)} activeOpacity={0.7} > @@ -488,152 +605,153 @@ const DayCell = React.memo(function DayCell({ day, label, dotColor, selected, to ); }); -const createStyles = (theme: ThemeColors) => StyleSheet.create({ - container: { - flex: 1, - }, - scrollContent: { - paddingBottom: 16, - flexGrow: 1, - }, - scrollBody: { - flex: 1, - }, - kbAvoid: { - flex: 1, - }, - calendarArea: { - paddingHorizontal: 12, - paddingTop: 6, - paddingBottom: 4, - }, - monthRow: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: 4, - paddingBottom: 14, - }, - monthNav: { - width: 36, - height: 36, - borderRadius: 12, - borderWidth: 1, - alignItems: 'center', - justifyContent: 'center', - }, - monthSelectorGroup: { - alignItems: 'flex-start', - flex: 1, - }, - monthButton: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - gap: 6, - paddingHorizontal: 10, - }, - monthLabel: { - fontSize: 22, - fontWeight: '700', - }, - yearButton: { - marginTop: -2, - paddingHorizontal: 6, - }, - yearLabel: { - fontSize: 17, - fontWeight: '700', - }, - weekdayRow: { - flexDirection: 'row', - marginBottom: 4, - paddingHorizontal: 2, - }, - weekday: { - flex: 1, - textAlign: 'center', - fontSize: 11, - fontWeight: '600', - textTransform: 'uppercase', - }, - weekRow: { - flexDirection: 'row', - gap: 6, - marginBottom: 6, - }, - dayCell: { - flex: 1, - height: 56, - borderRadius: 12, - borderWidth: 1, - borderColor: 'transparent', - paddingVertical: 4, - alignItems: 'center', - }, - dayNumber: { - fontSize: 14, - fontWeight: '600', - }, - dayNumberSelected: { - color: theme.text, - }, - chip: { - marginTop: 3, - paddingHorizontal: 4, - paddingVertical: 2, - borderRadius: 5, - maxWidth: '92%', - }, - chipText: { - color: '#FFFFFF', - fontSize: 8, - fontWeight: '600', - }, - chipPlaceholder: { - marginTop: 3, - width: 6, - height: 2, - borderRadius: 1, - opacity: 0.4, - }, - panelHeader: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: 18, - paddingTop: 15, - paddingBottom: 4, - }, - panelDate: { - fontSize: 16, - fontWeight: '700', - }, - panelCount: { - fontSize: 13, - }, - emptyState: { - alignItems: 'center', - paddingVertical: 36, - }, - emptyText: { - fontSize: 15, - fontWeight: '600', - marginBottom: 4, - }, - emptySubtext: { - fontSize: 13, - }, - backButton: { - padding: 8, - }, - eventCard: { - marginHorizontal: 16, - marginTop: 6, - }, - subtaskList: { - paddingLeft: 8, - paddingRight: 4, - paddingTop: 4, - }, -}); \ No newline at end of file +const createStyles = (theme: ThemeColors) => + StyleSheet.create({ + container: { + flex: 1, + }, + scrollContent: { + paddingBottom: 16, + flexGrow: 1, + }, + scrollBody: { + flex: 1, + }, + kbAvoid: { + flex: 1, + }, + calendarArea: { + paddingHorizontal: 12, + paddingTop: 6, + paddingBottom: 4, + }, + monthRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 4, + paddingBottom: 14, + }, + monthNav: { + width: 36, + height: 36, + borderRadius: 12, + borderWidth: 1, + alignItems: 'center', + justifyContent: 'center', + }, + monthSelectorGroup: { + alignItems: 'flex-start', + flex: 1, + }, + monthButton: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 6, + paddingHorizontal: 10, + }, + monthLabel: { + fontSize: 22, + fontWeight: '700', + }, + yearButton: { + marginTop: -2, + paddingHorizontal: 6, + }, + yearLabel: { + fontSize: 17, + fontWeight: '700', + }, + weekdayRow: { + flexDirection: 'row', + marginBottom: 4, + paddingHorizontal: 2, + }, + weekday: { + flex: 1, + textAlign: 'center', + fontSize: 11, + fontWeight: '600', + textTransform: 'uppercase', + }, + weekRow: { + flexDirection: 'row', + gap: 6, + marginBottom: 6, + }, + dayCell: { + flex: 1, + height: 56, + borderRadius: 12, + borderWidth: 1, + borderColor: 'transparent', + paddingVertical: 4, + alignItems: 'center', + }, + dayNumber: { + fontSize: 14, + fontWeight: '600', + }, + dayNumberSelected: { + color: theme.text, + }, + chip: { + marginTop: 3, + paddingHorizontal: 4, + paddingVertical: 2, + borderRadius: 5, + maxWidth: '92%', + }, + chipText: { + color: '#FFFFFF', + fontSize: 8, + fontWeight: '600', + }, + chipPlaceholder: { + marginTop: 3, + width: 6, + height: 2, + borderRadius: 1, + opacity: 0.4, + }, + panelHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 18, + paddingTop: 15, + paddingBottom: 4, + }, + panelDate: { + fontSize: 16, + fontWeight: '700', + }, + panelCount: { + fontSize: 13, + }, + emptyState: { + alignItems: 'center', + paddingVertical: 36, + }, + emptyText: { + fontSize: 15, + fontWeight: '600', + marginBottom: 4, + }, + emptySubtext: { + fontSize: 13, + }, + backButton: { + padding: 8, + }, + eventCard: { + marginHorizontal: 16, + marginTop: 6, + }, + subtaskList: { + paddingLeft: 8, + paddingRight: 4, + paddingTop: 4, + }, + }); diff --git a/carry-your-live/app/(tabs)/index.tsx b/carry-your-live/app/(tabs)/index.tsx index 4d00993..906c2e9 100644 --- a/carry-your-live/app/(tabs)/index.tsx +++ b/carry-your-live/app/(tabs)/index.tsx @@ -1,5 +1,13 @@ import React, { useCallback, useMemo, useState } from 'react'; -import { View, Text, StyleSheet, SafeAreaView, KeyboardAvoidingView, TouchableOpacity, BackHandler } from 'react-native'; +import { + View, + Text, + StyleSheet, + SafeAreaView, + KeyboardAvoidingView, + TouchableOpacity, + BackHandler, +} from 'react-native'; import { useFocusEffect } from 'expo-router'; import { Header } from '@/components/Header'; import { CategoryFilter } from '@/components/CategoryFilter'; @@ -39,7 +47,10 @@ export default function TasksScreen() { setShowCompleted(!showCompleted)} activeOpacity={0.8} @@ -57,18 +68,27 @@ export default function TasksScreen() { fill="none" /> {showCompleted && ( - + )} - + Completed - + @@ -103,4 +123,4 @@ const styles = StyleSheet.create({ kbAvoid: { flex: 1, }, -}); \ No newline at end of file +}); diff --git a/carry-your-live/app/(tabs)/settings.tsx b/carry-your-live/app/(tabs)/settings.tsx index 2bd484d..8112adc 100644 --- a/carry-your-live/app/(tabs)/settings.tsx +++ b/carry-your-live/app/(tabs)/settings.tsx @@ -1,5 +1,18 @@ import React, { useState, useCallback } from 'react'; -import { View, Text, StyleSheet, SafeAreaView, ScrollView, Switch, TouchableOpacity, Alert, Linking, Modal, Pressable, BackHandler } from 'react-native'; +import { + View, + Text, + StyleSheet, + SafeAreaView, + ScrollView, + Switch, + TouchableOpacity, + Alert, + Linking, + Modal, + Pressable, + BackHandler, +} from 'react-native'; import { useFocusEffect, useRouter } from 'expo-router'; import { Header } from '@/components/Header'; import { ListItem } from '@/components/ListItem'; @@ -10,7 +23,15 @@ import { FriendsModal } from '@/components/FriendsModal'; import { LegalModal } from '@/components/LegalModal'; import { ServerUrlModal } from '@/components/ServerUrlModal'; import SyncStatus from '@/components/SyncStatus'; -import { useSettings, SORT_OPTIONS, REMINDER_OPTIONS, ACCENT_PRESETS, DEFAULT_ACCENT, DEFAULT_ACCENT_OPACITY } from '@/theme'; +import { + useSettings, + SORT_OPTIONS, + REMINDER_OPTIONS, + ACCENT_PRESETS, + DEFAULT_ACCENT, + DEFAULT_ACCENT_OPACITY, + ThemeMode, +} from '@/theme'; import { useCategories } from '@/hooks/useDatabase'; import { getAuthUser, getAuthToken } from '@/services/auth'; import { checkForUpdates, getCurrentAppVersion } from '@/services/updates'; @@ -24,7 +45,34 @@ import { getLastVisitedTab, tabHref } from '@/utils/tabHistory'; export default function SettingsScreen() { const router = useRouter(); - const { theme, notifications, setNotifications, defaultCategoryId, setDefaultCategoryId, sortBy, setSortBy, reminderPreference, setReminderPreference, tagReminderPreferences, setTagReminderPreferences, apiUrl, accentColor, setAccentColor, accentOpacity, setAccentOpacity, todoAheadDays, setTodoAheadDays, showCompleted, setShowCompleted, deleteBySwipe, setDeleteBySwipe } = useSettings(); + const { + theme, + notifications, + setNotifications, + defaultCategoryId, + setDefaultCategoryId, + sortBy, + setSortBy, + reminderPreference, + setReminderPreference, + tagReminderPreferences, + setTagReminderPreferences, + apiUrl, + accentColor, + setAccentColor, + accentOpacity, + setAccentOpacity, + todoAheadDays, + setTodoAheadDays, + showCompleted, + setShowCompleted, + deleteBySwipe, + setDeleteBySwipe, + themeMode, + setThemeMode, + syncSettingsToServer, + loadSettingsFromServer, + } = useSettings(); const categories = useCategories(); const [picker, setPicker] = useState(null); const [reminderTagPicker, setReminderTagPicker] = useState(null); @@ -84,7 +132,9 @@ export default function SettingsScreen() { const lastSync = await getLastSyncTime(); if (user) { setSyncSubtitle( - lastSync ? `Signed in · synced ${formatSyncTime(lastSync)}` : `Signed in as ${user.username}` + lastSync + ? `Signed in · synced ${formatSyncTime(lastSync)}` + : `Signed in as ${user.username}` ); } else { setSyncSubtitle('Not signed in'); @@ -111,16 +161,15 @@ export default function SettingsScreen() { }, [refreshSyncStatus, router]) ); - const defaultCategory = categories.find((c) => c.id === defaultCategoryId); + const defaultCategory = categories.find(c => c.id === defaultCategoryId); const defaultCategoryLabel = defaultCategory?.name ?? 'None'; - const reminderLabel = REMINDER_OPTIONS.find((o) => o.value === reminderPreference)?.label ?? 'No reminder'; + const reminderLabel = + REMINDER_OPTIONS.find(o => o.value === reminderPreference)?.label ?? 'No reminder'; const tagReminderLabel = (categoryId: string) => { const selected = tagReminderPreferences[categoryId] ?? []; if (selected.length === 0) return `Default (${reminderLabel})`; - return selected - .map((v) => REMINDER_OPTIONS.find((o) => o.value === v)?.label ?? v) - .join(', '); + return selected.map(v => REMINDER_OPTIONS.find(o => o.value === v)?.label ?? v).join(', '); }; const openCustomAccent = () => { @@ -162,14 +211,12 @@ export default function SettingsScreen() {
Tags - {categories.map((category) => ( + {categories.map(category => ( - } + leftElement={} onPress={() => setEditingCategory(category)} showChevron /> @@ -181,7 +228,12 @@ export default function SettingsScreen() { > - + Add Tag @@ -193,7 +245,18 @@ export default function SettingsScreen() { rightElement={ { + setNotifications(value); + if (!value) { + const { cancelAllReminders } = await import('@/services/notifications'); + await cancelAllReminders(); + } else { + const { rescheduleAllReminders } = await import('@/services/notifications'); + const { collections } = await import('@/database'); + const tasks = await collections.tasks.query().fetch(); + await rescheduleAllReminders(tasks, true, reminderPreference); + } + }} thumbColor="#FFFFFF" trackColor={{ false: theme.borderStrong, true: theme.accent }} /> @@ -205,14 +268,12 @@ export default function SettingsScreen() { onPress={() => notifications && setPicker('reminder')} showChevron /> - {categories.map((category) => ( + {categories.map(category => ( - } + leftElement={} onPress={() => notifications && setReminderTagPicker(category)} showChevron /> @@ -226,7 +287,7 @@ export default function SettingsScreen() { /> o.value === sortBy)?.label ?? 'Date'} + subtitle={SORT_OPTIONS.find(o => o.value === sortBy)?.label ?? 'Date'} onPress={() => setPicker('sort')} showChevron /> @@ -264,7 +325,7 @@ export default function SettingsScreen() { Appearance Accent color - {ACCENT_PRESETS.map((c) => ( + {ACCENT_PRESETS.map(c => ( {accentColor.toUpperCase() === c && ( - + )} @@ -318,30 +386,19 @@ export default function SettingsScreen() { onPress={handleCheckUpdates} showChevron /> - - - setLegalVisible('privacy')} - showChevron - /> - setLegalVisible('terms')} - showChevron - /> + + + setLegalVisible('privacy')} showChevron /> + setLegalVisible('terms')} showChevron /> ({ value: c.id, label: c.name, color: c.color }))]} + options={[ + { value: '', label: 'None', color: theme.textMuted }, + ...categories.map(c => ({ value: c.id, label: c.name, color: c.color })), + ]} selectedValue={defaultCategoryId} onSelect={handleDefaultCategory} onClose={() => setPicker(null)} @@ -352,7 +409,7 @@ export default function SettingsScreen() { title="Sort Tasks By" options={SORT_OPTIONS} selectedValue={sortBy} - onSelect={(value) => setSortBy(value as typeof sortBy)} + onSelect={value => setSortBy(value as typeof sortBy)} onClose={() => setPicker(null)} /> @@ -365,15 +422,15 @@ export default function SettingsScreen() { onClose={() => setPicker(null)} /> - {categories.map((category) => ( + {categories.map(category => ( o.value !== 'none')} + options={REMINDER_OPTIONS.filter(o => o.value !== 'none')} selectedValue={tagReminderPreferences[category.id] ?? []} multiSelect - onSelect={(value) => handleTagReminderPreference(category.id, value)} + onSelect={value => handleTagReminderPreference(category.id, value)} onClose={() => setReminderTagPicker(null)} /> ))} @@ -394,41 +451,86 @@ export default function SettingsScreen() { onClose={() => setEditingCategory(null)} /> - { setSyncVisible(false); refreshSyncStatus(); }} /> + { + setSyncVisible(false); + refreshSyncStatus(); + }} + /> setFriendsVisible(false)} /> setServerUrlVisible(false)} /> - setCustomAccentVisible(false)}> - setCustomAccentVisible(false)} - > - {}}> + setCustomAccentVisible(false)} + > + setCustomAccentVisible(false)}> + {}} + > - Custom Accent Color - setCustomAccentVisible(false)} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}> + + Custom Accent Color + + setCustomAccentVisible(false)} + hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} + > - + - { setDraftAccent(color); setDraftAccentOpacity(opacity); }} /> + { + setDraftAccent(color); + setDraftAccentOpacity(opacity); + }} + /> { setDraftAccent(DEFAULT_ACCENT); setDraftAccentOpacity(DEFAULT_ACCENT_OPACITY); setAccentColor(DEFAULT_ACCENT); setAccentOpacity(DEFAULT_ACCENT_OPACITY); }} + onPress={() => { + setDraftAccent(DEFAULT_ACCENT); + setDraftAccentOpacity(DEFAULT_ACCENT_OPACITY); + setAccentColor(DEFAULT_ACCENT); + setAccentOpacity(DEFAULT_ACCENT_OPACITY); + }} activeOpacity={0.7} > - Reset + + Reset + { setAccentColor(draftAccent); setAccentOpacity(draftAccentOpacity); setCustomAccentVisible(false); }} + style={[ + styles.accentModalButton, + styles.accentModalButtonPrimary, + { backgroundColor: theme.accent }, + ]} + onPress={() => { + setAccentColor(draftAccent); + setAccentOpacity(draftAccentOpacity); + setCustomAccentVisible(false); + }} activeOpacity={0.8} > - Apply + + Apply + diff --git a/carry-your-live/app/(tabs)/stats.tsx b/carry-your-live/app/(tabs)/stats.tsx index 33ff95f..cd84bcf 100644 --- a/carry-your-live/app/(tabs)/stats.tsx +++ b/carry-your-live/app/(tabs)/stats.tsx @@ -15,7 +15,7 @@ export default function StatsScreen() { }, []) ); - const maxDaily = Math.max(1, ...stats.daily.map((d) => d.count)); + const maxDaily = Math.max(1, ...stats.daily.map(d => d.count)); return ( @@ -25,27 +25,42 @@ export default function StatsScreen() { Overview - {stats.totalCompleted} + + {stats.totalCompleted} + Completed - {stats.completedLast7} + + {stats.completedLast7} + Last 7 days - {stats.completedLast30} + + {stats.completedLast30} + Last 30 days - {stats.completionRate}% + + {stats.completionRate}% + Success rate - {stats.currentStreak} + + {stats.currentStreak} + Day streak - 0 ? theme.accent : theme.text }]}> + 0 ? theme.accent : theme.text }, + ]} + > {stats.overdueCount} Overdue @@ -64,7 +79,10 @@ export default function StatsScreen() { 0 ? theme.accent : theme.cardAlt }, + { + height: Math.max(4, height), + backgroundColor: d.count > 0 ? theme.accent : theme.cardAlt, + }, ]} /> @@ -78,9 +96,11 @@ export default function StatsScreen() { By tags {stats.byCategory.length === 0 ? ( - No completed tasks yet + + No completed tasks yet + ) : ( - stats.byCategory.map((c) => ( + stats.byCategory.map(c => ( {c.name} @@ -93,9 +113,11 @@ export default function StatsScreen() { By priority {stats.byPriority.length === 0 ? ( - No completed tasks yet + + No completed tasks yet + ) : ( - stats.byPriority.map((p) => ( + stats.byPriority.map(p => ( {p.label} @@ -206,4 +228,4 @@ const styles = StyleSheet.create({ emptyText: { fontSize: 14, }, -}); \ No newline at end of file +}); diff --git a/carry-your-live/app/_layout.tsx b/carry-your-live/app/_layout.tsx index ae6aa7e..07632b2 100644 --- a/carry-your-live/app/_layout.tsx +++ b/carry-your-live/app/_layout.tsx @@ -8,7 +8,12 @@ import { UpdateNotifier } from '@/components/UpdateNotifier'; import { Linking } from 'react-native'; import { useEffect } from 'react'; import { requestQuickAddFocus } from '@/utils/quickAddFocus'; -import { registerForPushNotificationsAsync, setupNotificationChannels, addNotificationResponseReceivedListener, removeNotificationSubscription } from '@/services/pushNotifications'; +import { + registerForPushNotificationsAsync, + setupNotificationChannels, + addNotificationResponseReceivedListener, + removeNotificationSubscription, +} from '@/services/pushNotifications'; import * as Notifications from 'expo-notifications'; Notifications.setNotificationHandler({ @@ -28,7 +33,7 @@ function RootNavigator() { setupNotificationChannels(); registerForPushNotificationsAsync().catch(() => {}); - const responseListener = addNotificationResponseReceivedListener((response) => { + const responseListener = addNotificationResponseReceivedListener(response => { const data = response.notification.request.content.data as Record | undefined; if (!data) return; @@ -53,7 +58,9 @@ function RootNavigator() { requestQuickAddFocus(); }; - Linking.getInitialURL().then(handleUrl).catch(() => {}); + Linking.getInitialURL() + .then(handleUrl) + .catch(() => {}); const subscription = Linking.addEventListener('url', ({ url }) => handleUrl(url)); const handleKeyDown = (event: KeyboardEvent) => { diff --git a/carry-your-live/app/add-task.tsx b/carry-your-live/app/add-task.tsx index aaa20f4..7de1925 100644 --- a/carry-your-live/app/add-task.tsx +++ b/carry-your-live/app/add-task.tsx @@ -1,5 +1,14 @@ import React, { useMemo, useRef } from 'react'; -import { View, Text, StyleSheet, SafeAreaView, ScrollView, KeyboardAvoidingView, Platform, Keyboard } from 'react-native'; +import { + View, + Text, + StyleSheet, + SafeAreaView, + ScrollView, + KeyboardAvoidingView, + Platform, + Keyboard, +} from 'react-native'; import { Header } from '@/components/Header'; import { CategorySelector } from '@/components/CategorySelector'; import { TaskNameInput } from '@/components/TaskNameInput'; @@ -112,15 +121,16 @@ export default function AddTaskScreen() { 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)}` - : ''; + const seriesId = + data.repeat !== 'none' + ? `series_${Math.random().toString(36).slice(2, 10)}${Date.now().toString(36)}` + : ''; const resolvedCategoryId = (data.tags && data.tags[0]) || ''; let createdTask: any = null; await database.write(async () => { - const task = await collections.tasks.create((t) => { + const task = await collections.tasks.create(t => { t.title = data.title.trim(); t.description = data.description || ''; t.categoryId = resolvedCategoryId; @@ -146,7 +156,7 @@ export default function AddTaskScreen() { for (let i = 0; i < data.subtasks.length; i++) { const subtask = data.subtasks[i]; if (subtask.title.trim()) { - await collections.subtasks.create((s) => { + await collections.subtasks.create(s => { s.taskId = task.id; s.title = subtask.title.trim(); s.description = ''; @@ -198,7 +208,7 @@ export default function AddTaskScreen() { > setValue('tags', value)} + onChange={value => setValue('tags', value)} error={errors.categoryId?.message} /> - setValue('priority', value)} - /> + setValue('priority', value)} /> setValue('reminders', value)} + onChange={value => setValue('reminders', value)} /> setValue('assigneeId', value)} - friends={friends.map((f) => f.username)} + friends={friends.map(f => f.username)} /> { const map = new Map(); for (const [taskId, roots] of subtasksByTask) { - const due = roots.filter((s) => s.dueDate && isDayMatch(s.dueDate, day)); + const due = roots.filter(s => s.dueDate && isDayMatch(s.dueDate, day)); if (due.length > 0) map.set(taskId, due); } return map; }, [subtasksByTask, day]); - const handleToggleComplete = useCallback(async (taskId: string) => { - await confirmAndToggleTaskComplete(taskId, () => { - refresh(); - refreshSubtasks(); - }); - }, [refresh, refreshSubtasks]); + const handleToggleComplete = useCallback( + async (taskId: string) => { + await confirmAndToggleTaskComplete(taskId, () => { + refresh(); + refreshSubtasks(); + }); + }, + [refresh, refreshSubtasks] + ); - const handleToggleSubtask = useCallback(async (subtaskId: string) => { - await toggleSubtaskComplete(subtaskId); - refreshSubtasks(); - }, [refreshSubtasks]); + const handleToggleSubtask = useCallback( + async (subtaskId: string) => { + await toggleSubtaskComplete(subtaskId); + refreshSubtasks(); + }, + [refreshSubtasks] + ); return ( - +
- + {loading ? ( Loading... ) : dayTasks.length === 0 && subtasksOnDay.size === 0 ? ( - No tasks on this day + + No tasks on this day + ) : ( <> - {dayTasks.map((task) => { + {dayTasks.map(task => { const subs = subtasksOnDay.get(task.id) ?? []; - const openCount = subs.filter((s) => !s.completed).length; - const cat = categories.find((c) => c.id === task.categoryId); + const openCount = subs.filter(s => !s.completed).length; + const cat = categories.find(c => c.id === task.categoryId); return ( - + - + ) : ( - + )} router.push({ pathname: '/task-detail', params: { id: task.id } })} + onPress={() => + router.push({ pathname: '/task-detail', params: { id: task.id } }) + } activeOpacity={0.7} > {task.title} {openCount > 0 && ( - + {openCount} open subtask{openCount === 1 ? '' : 's'} )} {!task.completed && ( {cat && ( - - {cat.name} + + + {cat.name} + )} {task.dueTime ? ( - {task.dueTime}{task.endTime ? `–${task.endTime}` : ''} + + {task.dueTime} + {task.endTime ? `–${task.endTime}` : ''} + ) : task.dueDate ? ( - {format(task.dueDate, 'HH:mm')} + + {format(task.dueDate, 'HH:mm')} + ) : null} )} @@ -152,7 +223,14 @@ export default function DayViewScreen() { accessibilityLabel={`Edit ${task.title}`} > - + @@ -160,10 +238,16 @@ export default function DayViewScreen() { ); })} {Array.from(subtasksOnDay.entries()).flatMap(([taskId, subs]) => { - const parent = dayTasks.find((t) => t.id === taskId); + const parent = dayTasks.find(t => t.id === taskId); if (parent) return []; - return subs.map((sub) => ( - + return subs.map(sub => ( + - + ) : ( - + )} router.push({ pathname: '/subtask-detail', params: { id: sub.id } })} + onPress={() => + router.push({ pathname: '/subtask-detail', params: { id: sub.id } }) + } activeOpacity={0.7} > {sub.title} {sub.dueTime ? ( - {sub.dueTime}{sub.endTime ? `–${sub.endTime}` : ''} + + {sub.dueTime} + {sub.endTime ? `–${sub.endTime}` : ''} + ) : null} @@ -205,89 +312,93 @@ export default function DayViewScreen() { )} - + {modals(() => {})} ); } -const createStyles = (theme: ThemeColors) => StyleSheet.create({ - container: { - flex: 1, - }, - kbAvoid: { - flex: 1, - }, - scrollBody: { - flex: 1, - }, - scrollContent: { - paddingTop: 12, - paddingBottom: 16, - }, - emptyState: { - alignItems: 'center', - paddingVertical: 64, - }, - emptyText: { - fontSize: 15, - fontWeight: '600', - }, - eventCard: { - marginHorizontal: 16, - marginBottom: 8, - borderRadius: 16, - borderWidth: 1, - paddingHorizontal: 14, - paddingVertical: 12, - }, - eventRow: { - flexDirection: 'row', - alignItems: 'center', - }, - checkCircle: { - width: 30, - marginRight: 12, - }, - eventTouch: { - flex: 1, - }, - eventTitle: { - fontSize: 15, - fontWeight: '500', - }, - eventCompleted: { - textDecorationLine: 'line-through', - color: theme.textMuted, - }, - eventSub: { - fontSize: 13, - marginTop: 2, - }, - metaRow: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, - marginTop: 4, - }, - tagChip: { - paddingHorizontal: 7, - paddingVertical: 2, - borderRadius: 6, - maxWidth: 140, - }, - tagText: { - color: '#FFFFFF', - fontSize: 10, - fontWeight: '600', - }, - timeText: { - fontSize: 12.5, - fontWeight: '500', - }, - menuButton: { - padding: 4, - marginLeft: 6, - }, -}); \ No newline at end of file +const createStyles = (theme: ThemeColors) => + StyleSheet.create({ + container: { + flex: 1, + }, + kbAvoid: { + flex: 1, + }, + scrollBody: { + flex: 1, + }, + scrollContent: { + paddingTop: 12, + paddingBottom: 16, + }, + emptyState: { + alignItems: 'center', + paddingVertical: 64, + }, + emptyText: { + fontSize: 15, + fontWeight: '600', + }, + eventCard: { + marginHorizontal: 16, + marginBottom: 8, + borderRadius: 16, + borderWidth: 1, + paddingHorizontal: 14, + paddingVertical: 12, + }, + eventRow: { + flexDirection: 'row', + alignItems: 'center', + }, + checkCircle: { + width: 30, + marginRight: 12, + }, + eventTouch: { + flex: 1, + }, + eventTitle: { + fontSize: 15, + fontWeight: '500', + }, + eventCompleted: { + textDecorationLine: 'line-through', + color: theme.textMuted, + }, + eventSub: { + fontSize: 13, + marginTop: 2, + }, + metaRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + marginTop: 4, + }, + tagChip: { + paddingHorizontal: 7, + paddingVertical: 2, + borderRadius: 6, + maxWidth: 140, + }, + tagText: { + color: '#FFFFFF', + fontSize: 10, + fontWeight: '600', + }, + timeText: { + fontSize: 12.5, + fontWeight: '500', + }, + menuButton: { + padding: 4, + marginLeft: 6, + }, + }); diff --git a/carry-your-live/app/subtask-detail.tsx b/carry-your-live/app/subtask-detail.tsx index 84ac639..d216979 100644 --- a/carry-your-live/app/subtask-detail.tsx +++ b/carry-your-live/app/subtask-detail.tsx @@ -1,5 +1,15 @@ import React from 'react'; -import { View, Text, StyleSheet, SafeAreaView, ScrollView, KeyboardAvoidingView, Platform, TouchableOpacity, Alert } from 'react-native'; +import { + View, + Text, + StyleSheet, + SafeAreaView, + ScrollView, + KeyboardAvoidingView, + Platform, + TouchableOpacity, + Alert, +} from 'react-native'; import { useLocalSearchParams, useRouter } from 'expo-router'; import { Header } from '@/components/Header'; import { TaskNameInput } from '@/components/TaskNameInput'; @@ -15,7 +25,14 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { useDatabase } from '@/hooks/useDatabase'; import { collections } from '@/database'; -import { SubtaskFormData, Reminder, parseReminders, toRemindersString, parseTaskTags, tagsToString } from '@/types'; +import { + SubtaskFormData, + Reminder, + parseReminders, + toRemindersString, + parseTaskTags, + tagsToString, +} from '@/types'; import { useSettings } from '@/theme'; import { updateSubtask, deleteSubtask } from '@/utils/taskActions'; import { CategorySelector } from '@/components/CategorySelector'; @@ -68,7 +85,14 @@ export default function SubtaskDetailScreen() { }, }); - const { handleSubmit, watch, setValue, control, reset, formState: { errors } } = methods; + const { + handleSubmit, + watch, + setValue, + control, + reset, + formState: { errors }, + } = methods; const priority = watch('priority'); const tags = watch('tags') ?? []; @@ -98,7 +122,10 @@ export default function SubtaskDetailScreen() { allDay: subtask.allDay ?? false, repeat: subtask.repeat, repeatInterval: subtask.repeatInterval || 1, - repeatDays: ((subtask.repeatDays || '') as string).split(',').map(Number).filter((d) => !Number.isNaN(d)), + repeatDays: ((subtask.repeatDays || '') as string) + .split(',') + .map(Number) + .filter(d => !Number.isNaN(d)), reminder: (subtask.reminder || 'none') as Reminder, reminders: subtask.reminders || '', assigneeId: subtask.assigneeId ?? null, @@ -160,7 +187,9 @@ export default function SubtaskDetailScreen() { if (!isReady || !loaded) { return ( - {notFound ? 'Subtask not found' : 'Loading...'} + + {notFound ? 'Subtask not found' : 'Loading...'} + ); } @@ -171,9 +200,23 @@ export default function SubtaskDetailScreen() { title="Edit Subtask" showLogo={true} rightAction={ - + - + } @@ -201,14 +244,8 @@ export default function SubtaskDetailScreen() { /> )} /> - setValue('tags', value)} - /> - setValue('priority', value)} - /> + setValue('tags', value)} /> + setValue('priority', value)} /> setValue('reminders', value)} + onChange={value => setValue('reminders', value)} /> setValue('assigneeId', value)} - friends={friends.map((f) => f.username)} + friends={friends.map(f => f.username)} /> - setExpanded(!expanded)} activeOpacity={0.8}> + setExpanded(!expanded)} + activeOpacity={0.8} + > {icon} {title} @@ -106,7 +130,14 @@ export default function TaskDetailScreen() { }, }); - const { handleSubmit, watch, setValue, control, reset, formState: { errors } } = methods; + const { + handleSubmit, + watch, + setValue, + control, + reset, + formState: { errors }, + } = methods; const tags = watch('tags') ?? []; const priority = watch('priority'); @@ -129,7 +160,8 @@ export default function TaskDetailScreen() { const subtasks = await collections.subtasks.query(Q.where('task_id', id)).fetch(); if (!mounted) return; - const remindersValue = task.reminders || (task.reminder && task.reminder !== 'none' ? task.reminder : ''); + const remindersValue = + task.reminders || (task.reminder && task.reminder !== 'none' ? task.reminder : ''); reset({ title: task.title, @@ -143,11 +175,14 @@ export default function TaskDetailScreen() { allDay: task.allDay ?? false, repeat: task.repeat, repeatInterval: task.repeatInterval || 1, - repeatDays: (task.repeatDays || '').split(',').map(Number).filter((d) => !Number.isNaN(d)), + repeatDays: (task.repeatDays || '') + .split(',') + .map(Number) + .filter(d => !Number.isNaN(d)), reminder: (task.reminder || 'none') as TaskFormData['reminder'], reminders: remindersValue, assigneeId: task.assigneeId ?? null, - subtasks: subtasks.map((s) => ({ title: s.title, _key: s.id })), + subtasks: subtasks.map(s => ({ title: s.title, _key: s.id })), }); setLoaded(true); } catch { @@ -155,7 +190,9 @@ export default function TaskDetailScreen() { } })(); - return () => { mounted = false; }; + return () => { + mounted = false; + }; }, [id, isReady, reset]); const onSubmit = async (data: TaskFormData) => { @@ -174,7 +211,7 @@ export default function TaskDetailScreen() { savedTask = task; const existingSubtasks = await collections.subtasks.query(Q.where('task_id', id)).fetch(); - await task.update((t) => { + await task.update(t => { t.title = data.title.trim(); t.description = data.description || ''; t.categoryId = (data.tags && data.tags[0]) || task.categoryId || ''; @@ -202,10 +239,10 @@ export default function TaskDetailScreen() { const trimmed = formItem.title.trim(); if (!trimmed) continue; if (formItem._key) { - const existing = existingSubtasks.find((s) => s.id === formItem._key && !s.parentSubtaskId); + const existing = existingSubtasks.find(s => s.id === formItem._key && !s.parentSubtaskId); if (existing) { keptIds.add(existing.id); - await existing.update((s) => { + await existing.update(s => { s.title = trimmed; s.order = order; s.updatedAt = now; @@ -214,7 +251,7 @@ export default function TaskDetailScreen() { continue; } } - await collections.subtasks.create((s) => { + await collections.subtasks.create(s => { s.taskId = task.id; s.categoryId = task.categoryId || ''; s.tags = task.tags || ''; @@ -244,9 +281,11 @@ export default function TaskDetailScreen() { if (existing.parentSubtaskId) continue; if (keptIds.has(existing.id)) continue; removedIds.push(existing.id); - const children = await collections.subtasks.query(Q.where('parent_subtask_id', existing.id)).fetch(); + const children = await collections.subtasks + .query(Q.where('parent_subtask_id', existing.id)) + .fetch(); for (const child of children) { - await child.update((c) => { + await child.update(c => { c.parentSubtaskId = null; c.updatedAt = now; }); @@ -288,9 +327,23 @@ export default function TaskDetailScreen() { title="Edit Task" showLogo={true} rightAction={ - + - + } @@ -310,7 +363,7 @@ export default function TaskDetailScreen() { > setValue('tags', value)} + onChange={value => setValue('tags', value)} error={errors.categoryId?.message} /> - setValue('priority', value)} - /> + setValue('priority', value)} /> - - - - - } defaultExpanded={!!dueDate}> + + + + + } + defaultExpanded={!!dueDate} + > - - - - } defaultExpanded={repeat !== 'none'}> + + + + } + defaultExpanded={repeat !== 'none'} + > - - - - } defaultExpanded={!!reminders && reminders !== ''}> + + + + } + defaultExpanded={!!reminders && reminders !== ''} + > setValue('reminders', value)} + onChange={value => setValue('reminders', value)} /> - - - - - } defaultExpanded={!!assigneeId}> + + + + + } + defaultExpanded={!!assigneeId} + > setValue('assigneeId', value)} - friends={friends.map((f) => f.username)} + friends={friends.map(f => f.username)} /> - - - - } defaultExpanded={!!methods.getValues('description')}> + + + + } + defaultExpanded={!!methods.getValues('description')} + > =12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/@expo/metro-file-map": { - "version": "57.0.1", - "resolved": "https://registry.npmjs.org/@expo/metro-file-map/-/metro-file-map-57.0.1.tgz", - "integrity": "sha512-8JXfVstZN7QnP4NianZZnlTVboOWR0sG8trUDNajOjnbGlPln29vponXM84tY+3tAHapz5/TxE53L0ixUwqPtA==", + "version": "57.0.2", + "resolved": "https://registry.npmjs.org/@expo/metro-file-map/-/metro-file-map-57.0.2.tgz", + "integrity": "sha512-tb50nSIWwKpRufSkGuivOK0FbUxv1Uwqptb0SzFTk0bkmYmcy3gIwnCbOHTfmrQDVndRhPEiu2SYgh9Z0PCvGg==", "license": "MIT", "dependencies": { "debug": "^4.3.4", @@ -2073,6 +2133,310 @@ "walker": "^1.0.8" } }, + "node_modules/@expo/metro-runtime": { + "version": "57.0.14", + "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-57.0.14.tgz", + "integrity": "sha512-LVpsfF0QOzlLSKlHYpxgW1cP1NKcYeaoalLO18jYHgqseSveEkb9VOffFpicTQ7AN03odZ4fgkUF+p5YpoY4yw==", + "license": "MIT", + "dependencies": { + "@expo/log-box": "^57.0.4", + "anser": "^1.4.9", + "pretty-format": "^29.7.0", + "stacktrace-parser": "^0.1.10", + "whatwg-fetch": "^3.0.0" + }, + "peerDependencies": { + "@expo/log-box": "^57.0.4", + "expo": "*", + "react": "*", + "react-dom": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/@expo/metro/node_modules/metro": { + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro/-/metro-0.84.5.tgz", + "integrity": "sha512-r1liLkyFZMVSEMNjU1CJU5pRzs3NdkxHqXS60O25c0rCIqAR+cGk7rPydw/g0WAIKVXojIBIF45yYBPagJGcgw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/core": "^7.25.2", + "@babel/generator": "^7.29.1", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "accepts": "^2.0.0", + "ci-info": "^2.0.0", + "connect": "^3.6.5", + "debug": "^4.4.0", + "error-stack-parser": "^2.0.6", + "flow-enums-runtime": "^0.0.6", + "graceful-fs": "^4.2.4", + "hermes-parser": "0.35.0", + "invariant": "^2.2.4", + "jest-worker": "^29.7.0", + "jsc-safe-url": "^0.2.2", + "lodash.throttle": "^4.1.1", + "metro-babel-transformer": "0.84.5", + "metro-cache": "0.84.5", + "metro-cache-key": "0.84.5", + "metro-config": "0.84.5", + "metro-core": "0.84.5", + "metro-file-map": "0.84.5", + "metro-resolver": "0.84.5", + "metro-runtime": "0.84.5", + "metro-source-map": "0.84.5", + "metro-symbolicate": "0.84.5", + "metro-transform-plugins": "0.84.5", + "metro-transform-worker": "0.84.5", + "mime-types": "^3.0.1", + "nullthrows": "^1.1.1", + "serialize-error": "^2.1.0", + "source-map": "^0.5.6", + "throat": "^5.0.0", + "ws": "^7.5.10", + "yargs": "^17.6.2" + }, + "bin": { + "metro": "src/cli.js" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@expo/metro/node_modules/metro-babel-transformer": { + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.84.5.tgz", + "integrity": "sha512-2WbHILKMiJUzfdjmGOQOqU1bWi9//gqiclc/tkk/AIsrrVw3efhZ1uhkOwMTxUEPOzqoo091H0olLmVZH5FHGQ==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "hermes-parser": "0.35.0", + "metro-cache-key": "0.84.5", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@expo/metro/node_modules/metro-cache": { + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.84.5.tgz", + "integrity": "sha512-WHS0n2OxQqtwEjSeQFPePNrMvEFhmQcUQM9cRJMHByWoi/GMWFBEWOf7hVkAM/0KRutAXNbDlSu/cZB6CyxgQQ==", + "license": "MIT", + "dependencies": { + "exponential-backoff": "^3.1.1", + "flow-enums-runtime": "^0.0.6", + "https-proxy-agent": "^7.0.5", + "metro-core": "0.84.5" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@expo/metro/node_modules/metro-cache-key": { + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.84.5.tgz", + "integrity": "sha512-3dPB2TnvGjjf0/9O7AXVQURKXuQNauTZE7WpTGTlR017Gh/B5y0m/2wcqxfveUguHSpu89KhVxCAlr2k/H7uhQ==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@expo/metro/node_modules/metro-config": { + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.84.5.tgz", + "integrity": "sha512-zie+uN6oohscowi2S7ByU+wUw6CrT4ZxW9uAbONOObSxx86RGmnIAmjXHLkfmcdYoY7jzOPEbqcI6oeVmqyBQA==", + "license": "MIT", + "dependencies": { + "connect": "^3.6.5", + "flow-enums-runtime": "^0.0.6", + "jest-validate": "^29.7.0", + "metro": "0.84.5", + "metro-cache": "0.84.5", + "metro-core": "0.84.5", + "metro-runtime": "0.84.5", + "yaml": "^2.6.1" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@expo/metro/node_modules/metro-core": { + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.84.5.tgz", + "integrity": "sha512-xwm605hCi5Y6eJTTb8ZWo6pkUcoBEIyiQOfkZh5GwtDwUrP9SNhTQZhzJHrBCwwxlf3Ptl/pxWJgQ1rsNYMnrA==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "lodash.throttle": "^4.1.1", + "metro-resolver": "0.84.5" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@expo/metro/node_modules/metro-file-map": { + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.84.5.tgz", + "integrity": "sha512-mlm/JL8toSbSc2akpKIGmzvrVRSCgZ5vkbycI34oMLoOnLGuLyC8WTyVJ6P0hZG/usDaGwZSl/s9BCRriqjGJA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fb-watchman": "^2.0.0", + "flow-enums-runtime": "^0.0.6", + "graceful-fs": "^4.2.4", + "invariant": "^2.2.4", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "nullthrows": "^1.1.1", + "walker": "^1.0.7" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@expo/metro/node_modules/metro-minify-terser": { + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.84.5.tgz", + "integrity": "sha512-BJoFwCEDsYnagPqarayInv2+diCDNDdLlaof/p6s9w4gh+gc9HXYM+pDvsKGKKUumpZswNF3Z/ftTMqKl/5IBg==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "terser": "^5.15.0" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@expo/metro/node_modules/metro-resolver": { + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.84.5.tgz", + "integrity": "sha512-VSSnepg1k6LyCwtb6eirWdAWlpKwBG8Rdtsr1mU38rMelFyWgh3/QuMSiZIZAIjwg/fsa8GhW5/FO54CAUPCEA==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@expo/metro/node_modules/metro-runtime": { + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.84.5.tgz", + "integrity": "sha512-U1m2+d1Pr+JO2/iVXBB2OfXXityz7tqwIorxfrT15IEgaHvpJBq/OHiqnOWPKJbUl3JcxjcdviZZOKk85oK4Qg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@expo/metro/node_modules/metro-source-map": { + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.84.5.tgz", + "integrity": "sha512-2BtV5L9uPc49F13Gn5wiP6bX/EncqzqTIk2VL/0F/96Vo0YEOjluT/qktQjFODfqGFsucwnh5mPEAl/2jVEfeg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "metro-symbolicate": "0.84.5", + "nullthrows": "^1.1.1", + "ob1": "0.84.5", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@expo/metro/node_modules/metro-symbolicate": { + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.84.5.tgz", + "integrity": "sha512-rQ40zYDAkaWBN9yvjUuAD0ZpzBMZSoKyGYXnb5JrfbKjun7fTvfoLHL3KXFYenBTYZkQtlp4cKSCv/1utxFyOw==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "metro-source-map": "0.84.5", + "nullthrows": "^1.1.1", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "bin": { + "metro-symbolicate": "src/index.js" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@expo/metro/node_modules/metro-transform-plugins": { + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.84.5.tgz", + "integrity": "sha512-+InaSVGaOyt0DyRo4Y/zIdPI6CZwnbNho5LAL23tgmuGwv7fyfkF7kKfPjZcfxXBcoYdTLLFnCfCH/dHSiCqNg==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/generator": "^7.29.1", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "flow-enums-runtime": "^0.0.6", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@expo/metro/node_modules/metro-transform-worker": { + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.84.5.tgz", + "integrity": "sha512-ui1Z8x4s5RL36gMmKLaMMO7O9NNDHNdthEZSCDQHAau3JcAsTaFOK6I+2q4I/kW5u8hSEjJk9L45TXSVJw6g1A==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/generator": "^7.29.1", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "flow-enums-runtime": "^0.0.6", + "metro": "0.84.5", + "metro-babel-transformer": "0.84.5", + "metro-cache": "0.84.5", + "metro-cache-key": "0.84.5", + "metro-minify-terser": "0.84.5", + "metro-source-map": "0.84.5", + "metro-transform-plugins": "0.84.5", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@expo/metro/node_modules/ob1": { + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.84.5.tgz", + "integrity": "sha512-aH9RkoZc7w/90HBamFxTw8ZLFr05wXS+iOnvmrgo53Ep8Pyrm5FieQSaPIVROkfFVQISeD/zo92fes26TOwe+A==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, "node_modules/@expo/osascript": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.7.1.tgz", @@ -2111,27 +2475,27 @@ } }, "node_modules/@expo/prebuild-config": { - "version": "57.0.10", - "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-57.0.10.tgz", - "integrity": "sha512-myrS5NolFAQWD8g7QuqkettnkyJx3GRl603N6rlmqAMxmO5EU7sZu4EX2EsIKkva8QtpX84B0E17PsM9xXMsTQ==", + "version": "57.0.15", + "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-57.0.15.tgz", + "integrity": "sha512-xTbWHroj0PDmlbqvmU+zF9ZZxveJkiuyiPoeRJYRGruFHebRAWnoTdw5S7d/UCzDBI8ropGGu9g2eb2nMxtvAw==", "license": "MIT", "dependencies": { - "@expo/config": "~57.0.6", - "@expo/config-plugins": "~57.0.6", + "@expo/config": "~57.0.9", + "@expo/config-plugins": "~57.0.9", "@expo/config-types": "^57.0.2", - "@expo/image-utils": "^0.11.4", + "@expo/image-utils": "^0.11.5", "@expo/json-file": "^11.0.1", - "@react-native/normalize-colors": "0.86.2", + "@react-native/normalize-colors": "0.86.3", "debug": "^4.3.1", - "expo-modules-autolinking": "~57.0.9", + "expo-modules-autolinking": "~57.0.12", "resolve-from": "^5.0.0", "semver": "^7.6.0" } }, "node_modules/@expo/require-utils": { - "version": "57.0.4", - "resolved": "https://registry.npmjs.org/@expo/require-utils/-/require-utils-57.0.4.tgz", - "integrity": "sha512-e7xbg/9BTQcsZE/oErafZXtI7kh5IgfasLJ97J5sFSzX2cA74pDvdlhW1KHVSaDkQyQv6h1LSLhsY7dEeOk7hw==", + "version": "57.0.5", + "resolved": "https://registry.npmjs.org/@expo/require-utils/-/require-utils-57.0.5.tgz", + "integrity": "sha512-kTAXj9lDFEIPMsbAOGCGbjBbMF0oi7CqkYM79KOX0DDD9wSwXmlKL1z2h8OwsrBf7mbOo2DjlRvZu4BEjrIxGw==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.20.0", @@ -2177,6 +2541,35 @@ "integrity": "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==", "license": "MIT" }, + "node_modules/@expo/ui": { + "version": "57.0.14", + "resolved": "https://registry.npmjs.org/@expo/ui/-/ui-57.0.14.tgz", + "integrity": "sha512-m8Z5dCplkOOQ70lu/xfB67qhk00Muw4U1T9frHBIW3LxxJeJZQ+jOmV6vCk6O6zVgoG/ib1et7PeX6etxI4Rsw==", + "license": "MIT", + "dependencies": { + "sf-symbols-typescript": "^2.1.0", + "vaul": "^1.1.2" + }, + "peerDependencies": { + "@babel/core": "*", + "expo": "*", + "react": "*", + "react-dom": "*", + "react-native": "*", + "react-native-worklets": "*" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native-worklets": { + "optional": true + } + } + }, "node_modules/@expo/xcpretty": { "version": "4.4.4", "resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.4.4.tgz", @@ -3023,31 +3416,31 @@ } }, "node_modules/@react-native/assets-registry": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.86.2.tgz", - "integrity": "sha512-vcX/mBjWAVnWofu7KecotquI2unZ/tITwA7OGdq/mdY/zmGXIEvYhfEYyOQij/LRqi9WAL+iizInTBWnxDhK/Q==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.86.3.tgz", + "integrity": "sha512-TDhgCZA4wjJg84d5A9swiOQYPIWSKEEVdg9IwMFZDupQzW/F3QoLUrfAJOcalgqTDA9/buTB8awhE3Whwg6u9Q==", "license": "MIT", "engines": { "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/@react-native/babel-plugin-codegen": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.86.2.tgz", - "integrity": "sha512-NNDZqOlNbH5SzgPks1jFDYH3234Rpa5e/nhZymxhIiBH3NcE3uD+rGj/HWXhH7nHF2ToGK6XbUpqy7nmJPeh+g==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.86.3.tgz", + "integrity": "sha512-O6Xza4JBGPIU8J7YbKTyBoYL4thpy8jMW/oaLDWdAyOwYHKIjK47pAL5HUEbOe2bWz2PEKjbYRF2ApkJv1ottQ==", "license": "MIT", "dependencies": { "@babel/traverse": "^7.29.0", - "@react-native/codegen": "0.86.2" + "@react-native/codegen": "0.86.3" }, "engines": { "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/@react-native/babel-preset": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.86.2.tgz", - "integrity": "sha512-4XKEJ6jKW9lXMB1O5o47gBoGgolde1fbX13gLW/erlcn+1ky+MHHo5UjuM3RWGdPJHIvIzekDDumSUHhB9x5iQ==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.86.3.tgz", + "integrity": "sha512-/eqs/Hy9RZRcjdcs4wj3Cqmxvtb3NM5g+Uuh1RIvsjynMO8PRsrVWWLWgBcZL/jYUo+ogXd2NFB0W8L5Bg89xw==", "license": "MIT", "peer": true, "dependencies": { @@ -3080,7 +3473,7 @@ "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", - "@react-native/babel-plugin-codegen": "0.86.2", + "@react-native/babel-plugin-codegen": "0.86.3", "babel-plugin-syntax-hermes-parser": "0.36.0", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" @@ -3093,9 +3486,9 @@ } }, "node_modules/@react-native/codegen": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.86.2.tgz", - "integrity": "sha512-xKkudsahUJ1n//55g4fXk5BStVqqmZlz8HQveL45ZxcfDnwvhuYe2GymksQANFsSN+slvrarjrfq8kIxJzbceA==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.86.3.tgz", + "integrity": "sha512-Ux4jHi0fh+bdtVEcL0gaPLbY56V+SvFUDl/8sRAE1jdb4k+o7fT/4Nc29yz4X+qfjstkSqObQTMBGhdzxH9JvA==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", @@ -3129,12 +3522,12 @@ } }, "node_modules/@react-native/community-cli-plugin": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.86.2.tgz", - "integrity": "sha512-YHXNKoM6Y/HjREySZ5arET2xgiHgg67r1MdwJB//MPJAJ0Xc5g0u6UHxY9VzsHO3Y07dre6s0BinYwjt1SEWvQ==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.86.3.tgz", + "integrity": "sha512-qSDL9LQc5mZSZPNczT95WU9YQuPzxBklgON9vLhhqfI0yWIwKInqFx88dQ/uiEXBtf0yossthaQIqA4Ml6bF6g==", "license": "MIT", "dependencies": { - "@react-native/dev-middleware": "0.86.2", + "@react-native/dev-middleware": "0.86.3", "debug": "^4.4.0", "invariant": "^2.2.4", "metro": "^0.84.3", @@ -3147,7 +3540,7 @@ }, "peerDependencies": { "@react-native-community/cli": "*", - "@react-native/metro-config": "0.86.2" + "@react-native/metro-config": "0.86.3" }, "peerDependenciesMeta": { "@react-native-community/cli": { @@ -3159,18 +3552,18 @@ } }, "node_modules/@react-native/debugger-frontend": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.86.2.tgz", - "integrity": "sha512-KGS1aV5F6cIqpnoIUhLBXyVzy1oAj8jBFGau6vX4Vy0HXRJN7p+68RU7x6NuyraHvQcR14ccMGT5TkFuNjQ4gA==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.86.3.tgz", + "integrity": "sha512-TQmeofQ0PcuylhhlleOeuzHYZfbrgm3gayXzowqUEzgRisTm1D40/J3ggqs7XkQi5HP5ZA3n8dHmKL9vIzPcsw==", "license": "BSD-3-Clause", "engines": { "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/@react-native/debugger-shell": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.86.2.tgz", - "integrity": "sha512-/TaVJ2+gGajZPJGrFaObUQmHmlaxAlfmOPZicl6pNKDUjzSgFMpcLkdTOExvb+USYTVdGX1XwxXyvjQdUO2bvg==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.86.3.tgz", + "integrity": "sha512-O4ds+J7xZfxkbih9T+cAGegBdvKSPKYJm/lDgC9CpEjFMkmzWTpVLU3Qsv9sqZuo58z+sGhIcJfPsCFFWHpqbQ==", "license": "MIT", "dependencies": { "cross-spawn": "^7.0.6", @@ -3182,14 +3575,14 @@ } }, "node_modules/@react-native/dev-middleware": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.86.2.tgz", - "integrity": "sha512-B7L0vKvg+IcEElT7Vpqh1xj5yJAqWUegjbP+bQRaorJMAYnv11GkliTnZV2AdTDfZQJWgOEx8i8LGkHkUg7bnA==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.86.3.tgz", + "integrity": "sha512-LiEPTqTg/63bYUnrPyHLfjTDCNhA/+CUqI1+DsA9tYyewtSbULd5awsva6SgE10I+2iMhgKXS3ymkhU/kSCrGA==", "license": "MIT", "dependencies": { "@isaacs/ttlcache": "^1.4.1", - "@react-native/debugger-frontend": "0.86.2", - "@react-native/debugger-shell": "0.86.2", + "@react-native/debugger-frontend": "0.86.3", + "@react-native/debugger-shell": "0.86.3", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.3.0", "connect": "^3.6.5", @@ -3205,32 +3598,32 @@ } }, "node_modules/@react-native/gradle-plugin": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.86.2.tgz", - "integrity": "sha512-2F6x14NcHMpVmfTTFKfMkpV5dZedZrLiv6PE+c3vgnesV2bjleUBydr4U+NI8VkI7OwW71L0A5qQ76I9LCrfoQ==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.86.3.tgz", + "integrity": "sha512-lxmx0GqLEWRIpZfpYFXlYVIs3ENQwaW6Vmp6oi29l2GoQJ1wZfFZRdMimDWlGEk8LKfHar3QH3iaPMkTcK9lEQ==", "license": "MIT", "engines": { "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/@react-native/js-polyfills": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.86.2.tgz", - "integrity": "sha512-bIwNcGBaQ74shB5z1mRkxOpjikimuwsnOCEkZSzL67Z1FTyK1ObpENfyd2QvcvVW9Cjl+tHuw9ynpBnb2jPoJQ==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.86.3.tgz", + "integrity": "sha512-eYIJ0es967+tePBFQDnl/gidVFxLns3fnbiK6rxscQrGodvuUO6hwxpQnfNynJ8MjbbndImXihXjcnJdc7SzJg==", "license": "MIT", "engines": { "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/@react-native/metro-babel-transformer": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.86.2.tgz", - "integrity": "sha512-mX1wgLErdb2hDgXJr9zM9SWLe+ZteZTFTwRWGOQ53yEJwc9DVSnxdTlAtVdMeOj2ntycKh0R8jYdat2Am43cwQ==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.86.3.tgz", + "integrity": "sha512-0nlwVkG0uT9o72nrGTKUz/IqDnCkM4zE0ihe4r+OJcxH93yBdDm58AXJ3EXZeSjv3osHEFXU7ceajdZiS34mTw==", "license": "MIT", "peer": true, "dependencies": { "@babel/core": "^7.25.2", - "@react-native/babel-preset": "0.86.2", + "@react-native/babel-preset": "0.86.3", "hermes-parser": "0.36.0", "nullthrows": "^1.1.1" }, @@ -3259,14 +3652,14 @@ } }, "node_modules/@react-native/metro-config": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.86.2.tgz", - "integrity": "sha512-hJno256j+MS0b3JD1aD3ouTGZVacKNVBuXL2atMQQ8BZ060vl1ptnZ83y569aDW+/rgFSOcqn6ydKeSz4uUKQQ==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.86.3.tgz", + "integrity": "sha512-qdzDMepV2xdUhsO4XC7idnnbt8K6+Hd59AjeK9y4KS86fxSQq5oL0TAbqUuajDGR7beNQoYTuS6AowN/zP0oaQ==", "license": "MIT", "peer": true, "dependencies": { - "@react-native/js-polyfills": "0.86.2", - "@react-native/metro-babel-transformer": "0.86.2", + "@react-native/js-polyfills": "0.86.3", + "@react-native/metro-babel-transformer": "0.86.3", "metro-config": "^0.84.3", "metro-runtime": "^0.84.3" }, @@ -3275,15 +3668,15 @@ } }, "node_modules/@react-native/normalize-colors": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.86.2.tgz", - "integrity": "sha512-EzPFc9Y6lzYOWeso2almwXI7f8+qReHxWvT+algsOczb2UhWXIWXDoSvkdwoSfiwwmGt/ijJgKJoeHlzPkLwRg==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.86.3.tgz", + "integrity": "sha512-Cv3CDkprb67GrzuaS9BGbBJC/6G4lIw3nyKOHRKTqTTum4bn37y5+R0Z04L8mcbQN85eEohNrRwb7IOM4j6uvg==", "license": "MIT" }, "node_modules/@react-native/virtualized-lists": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.86.2.tgz", - "integrity": "sha512-uO0J72gh3EvE+1/GHRk18QRyBDTRHRB0AraAfojsRjbT7VMuJwKrZYaKGshavoaEud6aw00ZB9/8mTMIKjjcAw==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.86.3.tgz", + "integrity": "sha512-1j44NEyNn05Ut40vHAmoSWbsIcybFkMAOBTwQt1PrESyfSS+qBoyU1LGIogNva0VIa0rQyEC5PzbA7R4/7Nhyw==", "license": "MIT", "dependencies": { "invariant": "^2.2.4", @@ -3295,7 +3688,7 @@ "peerDependencies": { "@types/react": "^19.2.0", "react": "*", - "react-native": "0.86.2" + "react-native": "0.86.3" }, "peerDependenciesMeta": { "@types/react": { @@ -3342,99 +3735,6 @@ "node": ">=10" } }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/dom/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@testing-library/dom/node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@testing-library/dom/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "license": "MIT", - "peer": true - }, - "node_modules/@testing-library/jest-dom": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", - "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", - "license": "MIT", - "dependencies": { - "@adobe/css-tools": "^4.4.0", - "aria-query": "^5.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.6.3", - "picocolors": "^1.1.1", - "redent": "^3.0.0" - }, - "engines": { - "node": ">=14", - "npm": ">=6", - "yarn": ">=1" - } - }, - "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "license": "MIT" - }, - "node_modules/@testing-library/user-event": { - "version": "14.6.3", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.3.tgz", - "integrity": "sha512-6dBq67jT8lE+JTE8Exm02Kt6ze43hz1jdiSpSJwtTZiT1xQQ6b7nZYTTQ9njdArdU8XklOwaDp/AbT/eYSKF4g==", - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" - } - }, "node_modules/@tybys/wasm-util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", @@ -3446,13 +3746,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "license": "MIT", - "peer": true - }, "node_modules/@types/cacheable-request": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", @@ -4269,9 +4562,9 @@ } }, "node_modules/agent-cli-detector": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/agent-cli-detector/-/agent-cli-detector-0.1.5.tgz", - "integrity": "sha512-6xvLw0EGPuxoYGZeqyMV4AK+WoZ31jsqb7a5pln1BTf6oDFsrgvfCJT7E7y9oAqifJZhJ3fZ0J36H7o6JzrB0Q==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/agent-cli-detector/-/agent-cli-detector-0.1.6.tgz", + "integrity": "sha512-vKrPeEVN3upDF3GjWxsWBbwQgMtNJ8VB1cduPvK3svmmz4ENpS7yaPxbogsbe3w+xp9xp2Cu+0Ar41rjAR4+lA==", "license": "MIT", "bin": { "agent-cli-detector": "dist/cli.js" @@ -4629,15 +4922,6 @@ "node": ">=10" } }, - "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" - } - }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", @@ -4989,9 +5273,9 @@ } }, "node_modules/babel-preset-expo": { - "version": "57.0.7", - "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-57.0.7.tgz", - "integrity": "sha512-/1RLnZTJVoTNo6nCdSv27BSA2LBzM/qEkNLznwWvztg64DhsAz7ByZIiAqdbau9FK3YqF+IV5a2ZsPXFclwq4A==", + "version": "57.0.9", + "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-57.0.9.tgz", + "integrity": "sha512-T19biGOBTnMp161PyBqWOoSIEeZug8/EjtuUdVob8JPQKZMTalHUQaQt+qtQQE7XsEkZJ6erb3k0CohneTFzQg==", "license": "MIT", "dependencies": { "@babel/generator": "^7.20.5", @@ -5030,7 +5314,7 @@ "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/preset-typescript": "^7.23.0", - "@react-native/babel-plugin-codegen": "0.86.2", + "@react-native/babel-plugin-codegen": "0.86.3", "babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-native-web": "~0.21.0", "babel-plugin-syntax-hermes-parser": "^0.36.0", @@ -5040,7 +5324,7 @@ "peerDependencies": { "@babel/runtime": "^7.20.0", "expo": "*", - "expo-widgets": "^57.0.10", + "expo-widgets": "^57.0.13", "react-refresh": ">=0.14.0 <1.0.0" }, "peerDependenciesMeta": { @@ -5833,12 +6117,6 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/css.escape": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", - "license": "MIT" - }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -6057,15 +6335,6 @@ "node": ">= 0.8" } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -6173,13 +6442,6 @@ "node": ">=0.10.0" } }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "license": "MIT", - "peer": true - }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -6856,9 +7118,9 @@ } }, "node_modules/eslint-config-expo": { - "version": "57.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-expo/-/eslint-config-expo-57.0.1.tgz", - "integrity": "sha512-OsLAReXOF3uXBwyxxw4f55sORYC+lOH4pSaGdU9MnYhRpjqPziK2uCnRxjLTGKeAKER7GSX5Wz9kNMGJT1MZEw==", + "version": "57.0.2", + "resolved": "https://registry.npmjs.org/eslint-config-expo/-/eslint-config-expo-57.0.2.tgz", + "integrity": "sha512-dOWkx+MWclLVnYpPPBzas4sfnPaAjk+fe/dbvJSzbWtHUR4fGGaTT/lTcOpxqhwRIlTKKBUYGKude0tcTBsZHg==", "dev": true, "license": "MIT", "dependencies": { @@ -7379,31 +7641,31 @@ } }, "node_modules/expo": { - "version": "57.0.10", - "resolved": "https://registry.npmjs.org/expo/-/expo-57.0.10.tgz", - "integrity": "sha512-nirZEdsA4ZKkzOZX3sqzpArc2tnVAvdiFmm0gZRmIckl0FnSgC3RGp27JJZ2NzqkEaVZ5e06dFvX/NECdK5cIQ==", + "version": "57.0.18", + "resolved": "https://registry.npmjs.org/expo/-/expo-57.0.18.tgz", + "integrity": "sha512-6nax9hJPhf9dWrstliXUABTJXDNIJrfJKcCtjSxuYVs2Yf127GIhKf3wsEYSFUl+LFdRbPPTNaweJePH159wZA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.0", - "@expo/cli": "^57.0.12", - "@expo/config": "~57.0.6", - "@expo/config-plugins": "~57.0.6", + "@expo/cli": "^57.0.20", + "@expo/config": "~57.0.9", + "@expo/config-plugins": "~57.0.9", "@expo/devtools": "~57.0.1", "@expo/dom-webview": "~57.0.1", - "@expo/fingerprint": "^0.20.6", - "@expo/local-build-cache-provider": "^57.0.5", - "@expo/log-box": "^57.0.2", - "@expo/metro": "~56.0.0", - "@expo/metro-config": "~57.0.7", + "@expo/fingerprint": "^0.20.11", + "@expo/local-build-cache-provider": "^57.0.8", + "@expo/log-box": "^57.0.4", + "@expo/metro": "~56.0.2", + "@expo/metro-config": "~57.0.12", "@ungap/structured-clone": "^1.3.0", - "babel-preset-expo": "~57.0.5", - "expo-asset": "~57.0.8", - "expo-constants": "~57.0.9", - "expo-file-system": "~57.0.1", - "expo-font": "~57.0.1", + "babel-preset-expo": "~57.0.9", + "expo-asset": "~57.0.15", + "expo-constants": "~57.0.16", + "expo-file-system": "~57.0.6", + "expo-font": "~57.0.2", "expo-keep-awake": "~57.0.1", - "expo-modules-autolinking": "~57.0.9", - "expo-modules-core": "~57.0.9", + "expo-modules-autolinking": "~57.0.12", + "expo-modules-core": "~57.0.14", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-minimum": "^0.1.2" @@ -7449,6 +7711,21 @@ "expo": "*" } }, + "node_modules/expo-asset": { + "version": "57.0.15", + "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-57.0.15.tgz", + "integrity": "sha512-ZoiUftQb1Nn1jvL6l1kZoUXvgRCx57tMQYxSsp+g4ZvY+jgVWp/THruXOtPtZVdC/WWZxPUTct53bfAm2EPDAQ==", + "license": "MIT", + "dependencies": { + "@expo/image-utils": "^0.11.5", + "expo-constants": "~57.0.15" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, "node_modules/expo-blur": { "version": "57.0.2", "resolved": "https://registry.npmjs.org/expo-blur/-/expo-blur-57.0.2.tgz", @@ -7461,12 +7738,12 @@ } }, "node_modules/expo-constants": { - "version": "57.0.9", - "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-57.0.9.tgz", - "integrity": "sha512-Y47sGiF+U8fwicUSPdJPjB27PuU+FgLK4Mpai0ksZF5hv8jNN3HBqKuDNxsiu70uHBKf80TaR4gwMPh0pmETiQ==", + "version": "57.0.16", + "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-57.0.16.tgz", + "integrity": "sha512-HG3yJjGZo2BPTo2F1sBoxdJZRwX7d9C554EhKn8m12K62pxGolCsvPSoh/lQf95OEP9vO34wwXB2C3QFs0KG0g==", "license": "MIT", "dependencies": { - "@expo/env": "~2.4.2" + "@expo/env": "~2.4.3" }, "peerDependencies": { "expo": "*", @@ -7474,13 +7751,13 @@ } }, "node_modules/expo-dev-client": { - "version": "57.0.10", - "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-57.0.10.tgz", - "integrity": "sha512-aY6PVbD1R+XwIcrOucRnB/yJfB6qVHIHqgRV6F8LwOzo7DwZaa/ixs+O2itHH7BNbUiwkS8tFjtbzJ4vDx/ulw==", + "version": "57.0.16", + "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-57.0.16.tgz", + "integrity": "sha512-Txss9sgzEFI0E+4I34E76vPiukeVDHnqCj0ZXy8SDijVM31/P6MCNniVbEZk7MBPQclD0aopfuaCFtkV8+r1sw==", "license": "MIT", "dependencies": { - "expo-dev-launcher": "~57.0.10", - "expo-dev-menu": "~57.0.10", + "expo-dev-launcher": "~57.0.16", + "expo-dev-menu": "~57.0.16", "expo-dev-menu-interface": "~57.0.0", "expo-manifests": "~57.0.1", "expo-updates-interface": "~57.0.1" @@ -7490,13 +7767,13 @@ } }, "node_modules/expo-dev-launcher": { - "version": "57.0.10", - "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-57.0.10.tgz", - "integrity": "sha512-LfGfiKDBzVgCXbIozbyzPU7yLdOtvmuH2U9jJbLEmkJxoExOy8NiFJoL8U+I/pYw3tj4I3GVkVOtZtItDFGn8Q==", + "version": "57.0.16", + "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-57.0.16.tgz", + "integrity": "sha512-jaBr6q4A5js/r465m2mRxN8A87YOi8I6PiIt8XIt4qfQWDgw6hqGovaF1AVFGT1Z5jxkjtgK8WNz/rAeypjqPQ==", "license": "MIT", "dependencies": { "@expo/schema-utils": "^57.0.2", - "expo-dev-menu": "~57.0.10", + "expo-dev-menu": "~57.0.16", "expo-manifests": "~57.0.1" }, "peerDependencies": { @@ -7505,9 +7782,9 @@ } }, "node_modules/expo-dev-menu": { - "version": "57.0.10", - "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-57.0.10.tgz", - "integrity": "sha512-y9S8J2MfqEO1T6daH3HNsjR7S4TcLf3CpSvNAwSzj5x6wmJoMYEvxWkzKy7Ep1R21EFBeSET1sZ1mFCCCYjgBw==", + "version": "57.0.16", + "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-57.0.16.tgz", + "integrity": "sha512-KzMHtAmnr0GqyU6MCrKLzIo/AfOjcToGhzK+pFeXlcqSPOrPJbTGlZqQ3E1o2G2jZMs+M1XvXh/VgnRyJUaRGw==", "license": "MIT", "dependencies": { "expo-dev-menu-interface": "~57.0.0" @@ -7565,15 +7842,25 @@ } }, "node_modules/expo-eas-client": { - "version": "57.0.1", - "resolved": "https://registry.npmjs.org/expo-eas-client/-/expo-eas-client-57.0.1.tgz", - "integrity": "sha512-4w51+zsl/ziUHQMJgLgUdgsNhRPAwHBfySpPB1hpWU21X74QS9T4SqDftaRnrDagn/DfcrXUMJvHbDQUxLPJNA==", + "version": "57.0.2", + "resolved": "https://registry.npmjs.org/expo-eas-client/-/expo-eas-client-57.0.2.tgz", + "integrity": "sha512-EfFiqUr0o9TvTOgMbqDiV1oIdG/d7kirhqtwa7roGmm9wF+CpXUz20d9YGzO6KzJWUYgdUgu4B6Ocv0jNJUdnQ==", "license": "MIT" }, + "node_modules/expo-file-system": { + "version": "57.0.6", + "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-57.0.6.tgz", + "integrity": "sha512-pm8PMYEW6BnVOCBJ7df9FcDmQtE1tqImuYphlfYe1ipQRLYtdCayRczJcbKIsnB3mqEyp4gC2gWmMbsAsAOjVg==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, "node_modules/expo-font": { - "version": "57.0.1", - "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-57.0.1.tgz", - "integrity": "sha512-QyS9L1Kh9sKJg4gfU6rdbpxpmH+DyzBX8z6jVvXMUDoqLr1GqmkO/Wu379KCXjL///kWbhpNlbi7AgBuj4VdIQ==", + "version": "57.0.2", + "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-57.0.2.tgz", + "integrity": "sha512-eWR0CAdysgVqg8VewJ8b5wKh1pa6C8YShIEU3SP0+hlSqOk4djUwuJjn3P04KHVzL3hws9FrUuPxCNmd4TGung==", "license": "MIT", "dependencies": { "fontfaceobserver": "^2.1.0" @@ -7611,12 +7898,12 @@ "license": "MIT" }, "node_modules/expo-linking": { - "version": "57.0.5", - "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-57.0.5.tgz", - "integrity": "sha512-SmJI3wr0EVfeKPGf+Qgr9gUbrXk8mM0ATqYLvAX/EAzawDjohPzMJ5pTt7TYMX0Wknj4XCtyCQrGhnERMXT6cQ==", + "version": "57.0.8", + "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-57.0.8.tgz", + "integrity": "sha512-gv0ZNaF7tILbcvX8xinNbQr6GIGQStxeUAJseweP+LjC9ped7Wl6EeyIMBlDR83U6Da+8drVeqBQSMvwAXgytA==", "license": "MIT", "dependencies": { - "expo-constants": "~57.0.9", + "expo-constants": "~57.0.15", "invariant": "^2.2.4" }, "peerDependencies": { @@ -7637,12 +7924,12 @@ } }, "node_modules/expo-modules-autolinking": { - "version": "57.0.9", - "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-57.0.9.tgz", - "integrity": "sha512-lj2nsAKMMRLXSFnGgaaQrWJ2fdSpLPc/bca6Rkiw4g8zYPn7qX4MRUJgavvzm/hBrzvMnlhXVgJtidOGuwBh+w==", + "version": "57.0.12", + "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-57.0.12.tgz", + "integrity": "sha512-Q8KAlq37nLKsQ+HsS9NpQVpd5jCgqtu694TDUNHBUBpV9ViD82mRBh8Uug/h68RG9xnLS+kuL4nYaCuFRghHjg==", "license": "MIT", "dependencies": { - "@expo/require-utils": "^57.0.4", + "@expo/require-utils": "^57.0.5", "@expo/spawn-async": "^1.8.0", "chalk": "^4.1.0", "commander": "^7.2.0" @@ -7652,13 +7939,13 @@ } }, "node_modules/expo-modules-core": { - "version": "57.0.9", - "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-57.0.9.tgz", - "integrity": "sha512-8UL/70VjmN8jOG4Tugdq6JX63Za6Qnri8h5dqFuLsDL2JxmTSB/Fz7fyEhDSpjlxVFyiJo5uF2aYVrz7Vxj2ZQ==", + "version": "57.0.14", + "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-57.0.14.tgz", + "integrity": "sha512-Cg3aQnVsQhZcMOF/RFgPGzMuhIak5W9eR9ecwmFSeQ813r4/49ut6RMA6PqUuUKuPCUIRqnBAHfSyTm1W9nfzg==", "license": "MIT", "dependencies": { "@expo/expo-modules-macros-plugin": "0.6.1", - "expo-modules-jsi": "~57.0.4", + "expo-modules-jsi": "~57.0.6", "invariant": "^2.2.4" }, "peerDependencies": { @@ -7673,25 +7960,25 @@ } }, "node_modules/expo-modules-jsi": { - "version": "57.0.4", - "resolved": "https://registry.npmjs.org/expo-modules-jsi/-/expo-modules-jsi-57.0.4.tgz", - "integrity": "sha512-vt7FyqUqqFXiRVnBqYD7y+GSPTgeua5Ocoy0+SYt+RSHkZEA2Fyop7If3g1TYDzQObYybPRo7TG2Rle1XLaWFw==", + "version": "57.0.6", + "resolved": "https://registry.npmjs.org/expo-modules-jsi/-/expo-modules-jsi-57.0.6.tgz", + "integrity": "sha512-WK0xFEe0FdZTCLCdfXK+HVNYfAmEbA6goecRxqjr0gh3XYRKAr9tikxqcM0ItLQoEPKntscPZhWcPLGvr97Tfw==", "license": "MIT", "peerDependencies": { "react-native": "*" } }, "node_modules/expo-notifications": { - "version": "57.0.8", - "resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-57.0.8.tgz", - "integrity": "sha512-9pTNFMB9vk7MmULRSq7SD2nBRXhIh7IYAOSAFzFIYna6csJKdzeuKikjuUwP5KZYwxejoaN2BYBBqdLKaUpT/w==", + "version": "57.0.15", + "resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-57.0.15.tgz", + "integrity": "sha512-H91z4WSFukSQP3dThgh6PCPFrT6VHeR+J2f7CuCOE72JV7YEGcBCir103QY58ocKsdgkodQpnkTU2m8LTHkWLg==", "license": "MIT", "dependencies": { - "@expo/image-utils": "^0.11.4", + "@expo/image-utils": "^0.11.5", "abort-controller": "^3.0.0", "badgin": "^1.1.5", "expo-application": "~57.0.2", - "expo-constants": "~57.0.8" + "expo-constants": "~57.0.15" }, "peerDependencies": { "expo": "*", @@ -7700,27 +7987,25 @@ } }, "node_modules/expo-router": { - "version": "57.0.10", - "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-57.0.10.tgz", - "integrity": "sha512-E6Cjudl4wQ86KdEHqLGhBmfOFRdtzXTtRzEEDmqOvqtkAc9C637u0us7CGKkkee9m+kwuHqfhn779ww85rn/Pg==", + "version": "57.0.17", + "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-57.0.17.tgz", + "integrity": "sha512-zKAfGLxrbdkUFzzW1xHezLXPSa0V4DlYKEWweDUUFGgnPWPNy3OY0EKYAqOhlBGSDebAvtnJXn+2zyyJjYKwyg==", "license": "MIT", "dependencies": { - "@expo/log-box": "^57.0.2", - "@expo/metro-runtime": "^57.0.8", + "@expo/log-box": "^57.0.4", + "@expo/metro-runtime": "^57.0.14", "@expo/schema-utils": "^57.0.2", - "@expo/ui": "^57.0.9", + "@expo/ui": "^57.0.14", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-tabs": "^1.1.12", "@react-native-masked-view/masked-view": "^0.3.2", - "@testing-library/jest-dom": "^6.9.1", - "@testing-library/user-event": "^14.6.1", "client-only": "^0.0.1", "color": "^4.2.3", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "expo-glass-effect": "^57.0.1", - "expo-server": "^57.0.1", - "expo-symbols": "^57.0.1", + "expo-server": "^57.0.3", + "expo-symbols": "^57.0.2", "fast-deep-equal": "^3.1.3", "invariant": "^2.2.4", "nanoid": "^3.3.8", @@ -7736,12 +8021,12 @@ "vaul": "^1.1.2" }, "peerDependencies": { - "@expo/log-box": "^57.0.2", - "@expo/metro-runtime": "^57.0.8", + "@expo/log-box": "^57.0.4", + "@expo/metro-runtime": "^57.0.14", "@testing-library/react-native": ">= 13.2.0", "expo": "*", - "expo-constants": "^57.0.9", - "expo-linking": "^57.0.5", + "expo-constants": "^57.0.15", + "expo-linking": "^57.0.8", "react": "*", "react-dom": "*", "react-native": "*", @@ -7773,60 +8058,6 @@ } } }, - "node_modules/expo-router/node_modules/@expo/metro-runtime": { - "version": "57.0.8", - "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-57.0.8.tgz", - "integrity": "sha512-RrdehKXNtWpnm8nNs1QFhS0IauyMKR/nSQiry6dcUJs2T5EH8gYDMcMegUt9yI41K4nO3W8tr3O0xd/GZFObXQ==", - "license": "MIT", - "dependencies": { - "@expo/log-box": "^57.0.2", - "anser": "^1.4.9", - "pretty-format": "^29.7.0", - "stacktrace-parser": "^0.1.10", - "whatwg-fetch": "^3.0.0" - }, - "peerDependencies": { - "@expo/log-box": "^57.0.2", - "expo": "*", - "react": "*", - "react-dom": "*", - "react-native": "*" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, - "node_modules/expo-router/node_modules/@expo/ui": { - "version": "57.0.9", - "resolved": "https://registry.npmjs.org/@expo/ui/-/ui-57.0.9.tgz", - "integrity": "sha512-VIxvk5ncgylBj2vrIP1iLaMc3XmYucKbf0hIcg3qx9l2anB9JzaYnH7cvVgNU3RfwV8R9m/tA7lX9BP7D8uMQw==", - "license": "MIT", - "dependencies": { - "sf-symbols-typescript": "^2.1.0", - "vaul": "^1.1.2" - }, - "peerDependencies": { - "@babel/core": "*", - "expo": "*", - "react": "*", - "react-dom": "*", - "react-native": "*", - "react-native-worklets": "*" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "react-native-worklets": { - "optional": true - } - } - }, "node_modules/expo-router/node_modules/@radix-ui/react-tabs": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.21.tgz", @@ -7878,18 +8109,18 @@ } }, "node_modules/expo-server": { - "version": "57.0.1", - "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-57.0.1.tgz", - "integrity": "sha512-sBfVDH6dmKVHZxqUxbfkzS00PZELMZt1IpnHKxcOTMZtR/t7CtRAFrbXcisG+EyzeqHSVDacZT+1tbYfZt5D8w==", + "version": "57.0.3", + "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-57.0.3.tgz", + "integrity": "sha512-aK+LdKzauHSGmsOStZtyxdzv0zWssCkxTw3m4QuOhfDSJsZaMRTd9O41d8ixU/QfELTbaJ0oRNcF7JFV/7O9YQ==", "license": "MIT", "engines": { "node": ">=20.16.0" } }, "node_modules/expo-sqlite": { - "version": "57.0.1", - "resolved": "https://registry.npmjs.org/expo-sqlite/-/expo-sqlite-57.0.1.tgz", - "integrity": "sha512-I6KoUvfGIiROTKxr5D3H+jRIGA/iEvWEtqHK4XMukAA7tVTinz/YdS8zOz6/DdG6vgrNmvF7gyOcbhLinlfxzQ==", + "version": "57.0.2", + "resolved": "https://registry.npmjs.org/expo-sqlite/-/expo-sqlite-57.0.2.tgz", + "integrity": "sha512-5KVbT7BQFZlIcQBXKWvwBERK3ODF6PSqct27GQKukZzAjsx2B97R+mwtYqrTUtUbw71VJrQrdr9Yl2OWYC7UpA==", "license": "MIT", "dependencies": { "await-lock": "^2.2.2" @@ -7918,9 +8149,9 @@ "license": "MIT" }, "node_modules/expo-symbols": { - "version": "57.0.1", - "resolved": "https://registry.npmjs.org/expo-symbols/-/expo-symbols-57.0.1.tgz", - "integrity": "sha512-8Zf+a83OywV0vf1NUtSKpNqKcULmO0GTI+zfFnGYl7SLDH9FjL5RcEZoy6CHvCgq2KDrQF21pl3r7Tb4ItPscw==", + "version": "57.0.2", + "resolved": "https://registry.npmjs.org/expo-symbols/-/expo-symbols-57.0.2.tgz", + "integrity": "sha512-qZ0iqOflm5lZGwRsQ5Y8sDksw3GAUKwHSX1bJBoocXf7gu14vafIXXYWte+JT9VfXAUGyKodJhLHP/GoOrcNWg==", "license": "MIT", "dependencies": { "@expo-google-fonts/material-symbols": "^0.4.1", @@ -7934,12 +8165,12 @@ } }, "node_modules/expo-system-ui": { - "version": "57.0.2", - "resolved": "https://registry.npmjs.org/expo-system-ui/-/expo-system-ui-57.0.2.tgz", - "integrity": "sha512-zABCRqFSioDBAo/RtmS0dQiGgDtDPZUVk01Y3Ti4ducobNM4HTM2sNAtq+YPpEUhaVW3hccPVsEUH4LK/ADVhA==", + "version": "57.0.3", + "resolved": "https://registry.npmjs.org/expo-system-ui/-/expo-system-ui-57.0.3.tgz", + "integrity": "sha512-3N3BoagGojEvQw0h7kL0Lvv2LrVjqEvODSNS+Nc9lj/kzaTaVx4K1HI8qqxeALMf5jui3rLP2b3Ai2pXotoNuA==", "license": "MIT", "dependencies": { - "@react-native/normalize-colors": "0.86.2", + "@react-native/normalize-colors": "0.86.3", "debug": "^4.3.2" }, "peerDependencies": { @@ -7954,9 +8185,9 @@ } }, "node_modules/expo-updates": { - "version": "57.0.12", - "resolved": "https://registry.npmjs.org/expo-updates/-/expo-updates-57.0.12.tgz", - "integrity": "sha512-ZFsW8Mi9qFrrYPSXF++1FXejjflRdgbVPzaSJJATuHelVmIIb3D98gCO9sIsaLPHO1RCQVj1ltkj51VnwVL+4g==", + "version": "57.0.19", + "resolved": "https://registry.npmjs.org/expo-updates/-/expo-updates-57.0.19.tgz", + "integrity": "sha512-xX4KIUa8H2xKmiVDe/uEehD96MV6nyyF/OCrE+PBYfOEp6BbS8bYkHZfNFcoDCKMRuT6pg9JrCi8OmnoWlM/2A==", "license": "MIT", "dependencies": { "@expo/code-signing-certificates": "^0.0.6", @@ -7965,7 +8196,7 @@ "arg": "^4.1.0", "chalk": "^4.1.2", "debug": "^4.3.4", - "expo-eas-client": "~57.0.1", + "expo-eas-client": "~57.0.2", "expo-manifests": "~57.0.1", "expo-structured-headers": "~57.0.0", "expo-updates-interface": "~57.0.1", @@ -8006,36 +8237,36 @@ "license": "MIT" }, "node_modules/expo/node_modules/@expo/cli": { - "version": "57.0.12", - "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-57.0.12.tgz", - "integrity": "sha512-mmOcZJmyEDYtEtHr5e4mBr8O+Y2WBSIhqY8PL5uY3EjQts/5vxKNBYuUqIknGUvZHGpdIjAQ1IjaPNQiAf8uPQ==", + "version": "57.0.20", + "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-57.0.20.tgz", + "integrity": "sha512-ZjWz7SA5TBTyKq4+aFRUPgMFxmqHtKkDMv6d85J2UAAjdRhkNRf+B80t+rr5IFo2ywuTvyrPRBth4ei4w08olA==", "license": "MIT", "dependencies": { "@expo/code-signing-certificates": "^0.0.6", - "@expo/config": "~57.0.6", - "@expo/config-plugins": "~57.0.6", + "@expo/config": "~57.0.9", + "@expo/config-plugins": "~57.0.9", "@expo/devcert": "^1.2.1", - "@expo/env": "~2.4.2", - "@expo/image-utils": "^0.11.4", - "@expo/inline-modules": "^0.1.4", + "@expo/env": "~2.4.3", + "@expo/image-utils": "^0.11.5", + "@expo/inline-modules": "^0.1.7", "@expo/json-file": "^11.0.1", - "@expo/log-box": "^57.0.2", - "@expo/metro": "~56.0.0", - "@expo/metro-config": "~57.0.7", - "@expo/metro-file-map": "^57.0.1", + "@expo/log-box": "^57.0.4", + "@expo/metro": "~56.0.2", + "@expo/metro-config": "~57.0.12", + "@expo/metro-file-map": "^57.0.2", "@expo/osascript": "^2.7.1", "@expo/package-manager": "^1.13.1", "@expo/plist": "^0.8.1", - "@expo/prebuild-config": "^57.0.10", - "@expo/require-utils": "^57.0.4", - "@expo/router-server": "^57.0.5", + "@expo/prebuild-config": "^57.0.15", + "@expo/require-utils": "^57.0.5", + "@expo/router-server": "^57.0.8", "@expo/schema-utils": "^57.0.2", "@expo/spawn-async": "^1.8.0", "@expo/ws-tunnel": "^2.0.0", "@expo/xcpretty": "^4.4.4", - "@react-native/dev-middleware": "0.86.2", + "@react-native/dev-middleware": "0.86.3", "accepts": "^1.3.8", - "agent-cli-detector": "^0.1.2", + "agent-cli-detector": "0.1.6", "arg": "^5.0.2", "bplist-creator": "0.1.0", "bplist-parser": "^0.3.1", @@ -8045,12 +8276,12 @@ "connect": "^3.7.0", "debug": "^4.3.4", "dnssd-advertise": "^1.1.4", - "expo-server": "^57.0.1", + "expo-server": "^57.0.3", "fetch-nodeshim": "^0.4.10", "getenv": "^2.0.0", "glob": "^13.0.0", "lan-network": "^0.2.1", - "multitars": "^1.0.0", + "multitars": "^1.0.2", "node-forge": "^1.3.3", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", @@ -8059,6 +8290,7 @@ "progress": "^2.0.3", "prompts": "^2.3.2", "resolve-from": "^5.0.0", + "sandbox-cli-detector": "^0.2.0", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", @@ -8088,20 +8320,20 @@ } }, "node_modules/expo/node_modules/@expo/cli/node_modules/@expo/router-server": { - "version": "57.0.5", - "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-57.0.5.tgz", - "integrity": "sha512-vke39l0bo3H2q9JB/KXpAJ7HpscdTG3Mktbxanc8yn3riWzzSsnv0uxwZGZCrZrnDQzFxlLTXgbZrGkU26ng1w==", + "version": "57.0.8", + "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-57.0.8.tgz", + "integrity": "sha512-lEPGYoDmh97yyEcY/MFWQsfE7yIgMRnGDcnsBN7B6AiWBFSJIMEcGnO0aYPgY0ZjNWyJDi7Ee8QZRGqB+LhI6w==", "license": "MIT", "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { - "@expo/metro-runtime": "^57.0.8", + "@expo/metro-runtime": "^57.0.14", "expo": "*", - "expo-constants": "^57.0.9", + "expo-constants": "^57.0.15", "expo-font": "^57.0.1", "expo-router": "*", - "expo-server": "^57.0.1", + "expo-server": "^57.0.3", "react": "*", "react-dom": "*", "react-server-dom-webpack": "~19.0.1 || ~19.1.2 || ~19.2.1" @@ -8121,45 +8353,6 @@ } } }, - "node_modules/expo/node_modules/@expo/metro-config": { - "version": "57.0.7", - "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-57.0.7.tgz", - "integrity": "sha512-bVfEkg4zF1cA62OqAdYXmFOooJ6TB/I+REi7Se6Ct+PbSC+89TwSqWXnYx34L08eIs4z+1ilgbATakTZpgefmQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.20.0", - "@babel/core": "^7.20.0", - "@babel/generator": "^7.20.5", - "@expo/config": "~57.0.6", - "@expo/env": "~2.4.2", - "@expo/json-file": "~11.0.1", - "@expo/metro": "~56.0.0", - "@expo/require-utils": "^57.0.4", - "@expo/spawn-async": "^1.8.0", - "@jridgewell/gen-mapping": "^0.3.13", - "@jridgewell/remapping": "^2.3.5", - "@jridgewell/sourcemap-codec": "^1.5.5", - "browserslist": "^4.25.0", - "chalk": "^4.1.0", - "debug": "^4.3.2", - "getenv": "^2.0.0", - "glob": "^13.0.0", - "hermes-parser": "^0.36.0", - "jsc-safe-url": "^0.2.4", - "lightningcss": "^1.30.1", - "picomatch": "^4.0.4", - "postcss": "^8.5.14", - "resolve-from": "^5.0.0" - }, - "peerDependencies": { - "expo": "*" - }, - "peerDependenciesMeta": { - "expo": { - "optional": true - } - } - }, "node_modules/expo/node_modules/@expo/ws-tunnel": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@expo/ws-tunnel/-/ws-tunnel-2.0.0.tgz", @@ -8197,31 +8390,6 @@ "node": ">=8" } }, - "node_modules/expo/node_modules/expo-asset": { - "version": "57.0.8", - "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-57.0.8.tgz", - "integrity": "sha512-sGocG+Wd2WcZ1KjKyB2LfUZXzrBM0VHEATGcpJKF9T3HM56M/sMbH73vv+n85u5Zr0FkJjEbV1DWhGPimCR1QQ==", - "license": "MIT", - "dependencies": { - "@expo/image-utils": "^0.11.4", - "expo-constants": "~57.0.8" - }, - "peerDependencies": { - "expo": "*", - "react": "*", - "react-native": "*" - } - }, - "node_modules/expo/node_modules/expo-file-system": { - "version": "57.0.1", - "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-57.0.1.tgz", - "integrity": "sha512-w7/ERvQFrGP2apTO9lDtZ+O6JQIhfakL7+Xqzh+rfMO9B4LB4qwrz+YvLgir8KFRVX64JHBnRuYBVLY1oQZcqw==", - "license": "MIT", - "peerDependencies": { - "expo": "*", - "react-native": "*" - } - }, "node_modules/expo/node_modules/expo-keep-awake": { "version": "57.0.1", "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-57.0.1.tgz", @@ -8232,21 +8400,6 @@ "react": "*" } }, - "node_modules/expo/node_modules/hermes-estree": { - "version": "0.36.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.36.1.tgz", - "integrity": "sha512-guv1nQ6IJ7S83NRFPWc3SA7IBZrdNC9kapwOq6uXvF4wP+sDCgjzQbKPCoyYmoyZRzztF/n/c36l/rccCZSiCw==", - "license": "MIT" - }, - "node_modules/expo/node_modules/hermes-parser": { - "version": "0.36.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.36.1.tgz", - "integrity": "sha512-GApNk4zLHi2UWoWZZkx7LNCOSzLSc5lB55pZ/PhK7ycFeg7u5LcF88p/WbpIi1XUDtE0MpHE3uRR3u3KB7TjSQ==", - "license": "MIT", - "dependencies": { - "hermes-estree": "0.36.1" - } - }, "node_modules/expo/node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -8278,9 +8431,9 @@ } }, "node_modules/expo/node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "license": "MIT", "engines": { "node": ">=12" @@ -8290,9 +8443,9 @@ } }, "node_modules/expo/node_modules/ws": { - "version": "8.21.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.2.tgz", - "integrity": "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -9118,9 +9271,9 @@ } }, "node_modules/hermes-compiler": { - "version": "250829098.0.16", - "resolved": "https://registry.npmjs.org/hermes-compiler/-/hermes-compiler-250829098.0.16.tgz", - "integrity": "sha512-xsgzk+mUyvt9t1nUbF8USBlYxajTUtPJhVZ86q85s/SEoMKCF+52YZcudb0ENSnV3T3lV9mgB3s6R7+pH90zgw==", + "version": "250829098.0.17", + "resolved": "https://registry.npmjs.org/hermes-compiler/-/hermes-compiler-250829098.0.17.tgz", + "integrity": "sha512-qG1PXzTEtriF6oQLZF3vyHhSMxOdW5h2TqqLri0rdpstPustd2fSvRZQMVAPdlhgFwBfYnj3OUZtiO6LjYsEFw==", "license": "MIT" }, "node_modules/hermes-estree": { @@ -9315,15 +9468,6 @@ "node": ">=0.8.19" } }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -10659,16 +10803,6 @@ "yallist": "^3.0.2" } }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "license": "MIT", - "peer": true, - "bin": { - "lz-string": "bin/bin.js" - } - }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -11075,15 +11209,6 @@ "node": ">=4" } }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/minimatch": { "version": "10.2.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", @@ -11150,9 +11275,9 @@ "license": "MIT" }, "node_modules/multitars": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/multitars/-/multitars-1.0.0.tgz", - "integrity": "sha512-H/J4fMLedtudftaYMOg7ajzLYgT3/rwbWVJbqr/iUgB8DQztn38ys5HOqI1CzSxx8QhXXwOOnnBvd4v3jG5+Mg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/multitars/-/multitars-1.0.2.tgz", + "integrity": "sha512-6GwVw5eLi9sThdtlS4PKwC7yRLaf45pYhIEzKBHdKxi+YOXGKFX8acIniH+Uh/+k9mS2lQOupTccjoe5r0/1IQ==", "license": "MIT" }, "node_modules/nanoid": { @@ -12059,9 +12184,9 @@ } }, "node_modules/postcss": { - "version": "8.5.25", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", - "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "funding": [ { "type": "opencollective", @@ -12078,7 +12203,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.16", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -12428,18 +12553,18 @@ "license": "MIT" }, "node_modules/react-native": { - "version": "0.86.2", - "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.86.2.tgz", - "integrity": "sha512-zbJXGZpwfZGA79Z9ob6Atvfx4nAQL8yJBa35s58E4Oo+khPykfQP2sTeumkKbjwajFYfVayg8pj7Il9nIfTk7A==", + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.86.3.tgz", + "integrity": "sha512-JR5s3bM9ezud+Mw24GlNXNfthqPIKwrQgPPJcam+L97t2sKjjEavhCzBn+fyqZZRcM5+XlhYxpTxVkK7e1n38Q==", "license": "MIT", "dependencies": { - "@react-native/assets-registry": "0.86.2", - "@react-native/codegen": "0.86.2", - "@react-native/community-cli-plugin": "0.86.2", - "@react-native/gradle-plugin": "0.86.2", - "@react-native/js-polyfills": "0.86.2", - "@react-native/normalize-colors": "0.86.2", - "@react-native/virtualized-lists": "0.86.2", + "@react-native/assets-registry": "0.86.3", + "@react-native/codegen": "0.86.3", + "@react-native/community-cli-plugin": "0.86.3", + "@react-native/gradle-plugin": "0.86.3", + "@react-native/js-polyfills": "0.86.3", + "@react-native/normalize-colors": "0.86.3", + "@react-native/virtualized-lists": "0.86.3", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", @@ -12447,7 +12572,7 @@ "base64-js": "^1.5.1", "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", - "hermes-compiler": "250829098.0.16", + "hermes-compiler": "250829098.0.17", "invariant": "^2.2.4", "memoize-one": "^5.0.0", "metro-runtime": "^0.84.3", @@ -12473,7 +12598,7 @@ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" }, "peerDependencies": { - "@react-native/jest-preset": "0.86.2", + "@react-native/jest-preset": "0.86.3", "@types/react": "^19.1.1", "react": "^19.2.3" }, @@ -12786,19 +12911,6 @@ "dev": true, "license": "MIT" }, - "node_modules/redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "license": "MIT", - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -13203,6 +13315,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sandbox-cli-detector": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/sandbox-cli-detector/-/sandbox-cli-detector-0.2.0.tgz", + "integrity": "sha512-4lyHX0ZU0AZKwjgZ1InxZAa3PNpyEb8rOQ+Zss1ReYmhNzW0Q+h1zE5nvniXN0HaAWZaZE1zgVNEirb0R7LmNg==", + "license": "MIT", + "bin": { + "sandbox-cli-detector": "dist/cli.js" + }, + "engines": { + "node": ">=18.18" + } + }, "node_modules/sanitize-filename": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", @@ -13899,18 +14023,6 @@ "node": ">=4" } }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "license": "MIT", - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", diff --git a/carry-your-live/package.json b/carry-your-live/package.json index 59a07e0..76f4a5f 100644 --- a/carry-your-live/package.json +++ b/carry-your-live/package.json @@ -9,23 +9,23 @@ "@react-native-async-storage/async-storage": "2.2.0", "@react-native-community/datetimepicker": "9.1.0", "date-fns": "^3.6.0", - "expo": "~57.0.10", + "expo": "~57.0.18", "expo-blur": "~57.0.2", "expo-constants": "~57.0.9", - "expo-dev-client": "~57.0.1", + "expo-dev-client": "~57.0.16", "expo-device": "^57.0.1", "expo-intent-launcher": "~57.0.1", - "expo-linking": "~57.0.5", - "expo-notifications": "~57.0.8", - "expo-router": "~57.0.10", - "expo-sqlite": "~57.0.1", + "expo-linking": "~57.0.8", + "expo-notifications": "~57.0.15", + "expo-router": "~57.0.17", + "expo-sqlite": "~57.0.2", "expo-status-bar": "~57.0.1", - "expo-system-ui": "~57.0.2", - "expo-updates": "~57.0.12", + "expo-system-ui": "~57.0.3", + "expo-updates": "~57.0.19", "react": "19.2.3", "react-dom": "19.2.3", "react-hook-form": "^7.51.5", - "react-native": "0.86.2", + "react-native": "0.86.3", "react-native-gesture-handler": "2.32.0", "react-native-keyboard-controller": "1.21.9", "react-native-safe-area-context": "5.7.0", @@ -41,7 +41,7 @@ "electron": "^41.7.1", "electron-builder": "^26.15.3", "eslint": "^9.0.0", - "eslint-config-expo": "~57.0.1", + "eslint-config-expo": "~57.0.2", "patch-package": "^8.0.1", "prettier": "^3.9.6", "typescript": "~6.0.3" diff --git a/carry-your-live/patches/react-native+0.86.3.patch b/carry-your-live/patches/react-native+0.86.3.patch new file mode 100644 index 0000000..c95c8d8 --- /dev/null +++ b/carry-your-live/patches/react-native+0.86.3.patch @@ -0,0 +1,78 @@ +diff --git a/node_modules/react-native/src/private/webapis/dom/events/Event.js b/node_modules/react-native/src/private/webapis/dom/events/Event.js +index f918f97..5deab46 100644 +--- a/node_modules/react-native/src/private/webapis/dom/events/Event.js ++++ b/node_modules/react-native/src/private/webapis/dom/events/Event.js +@@ -70,7 +70,7 @@ export default class Event { + [CURRENT_TARGET_KEY]: EventTarget | null = null; + + // $FlowExpectedError[unsupported-syntax] +- [EVENT_PHASE_KEY]: boolean = Event.NONE; ++ [EVENT_PHASE_KEY]: boolean = 0; + + // $FlowExpectedError[unsupported-syntax] + [IN_PASSIVE_LISTENER_FLAG_KEY]: boolean = false; +@@ -193,48 +193,64 @@ export default class Event { + + // $FlowExpectedError[cannot-write] + Object.defineProperty(Event, 'NONE', { ++ writable: true, ++ configurable: true, + enumerable: true, + value: 0, + }); + + // $FlowExpectedError[cannot-write] + Object.defineProperty(Event.prototype, 'NONE', { ++ writable: true, ++ configurable: true, + enumerable: true, + value: 0, + }); + + // $FlowExpectedError[cannot-write] + Object.defineProperty(Event, 'CAPTURING_PHASE', { ++ writable: true, ++ configurable: true, + enumerable: true, + value: 1, + }); + + // $FlowExpectedError[cannot-write] + Object.defineProperty(Event.prototype, 'CAPTURING_PHASE', { ++ writable: true, ++ configurable: true, + enumerable: true, + value: 1, + }); + + // $FlowExpectedError[cannot-write] + Object.defineProperty(Event, 'AT_TARGET', { ++ writable: true, ++ configurable: true, + enumerable: true, + value: 2, + }); + + // $FlowExpectedError[cannot-write] + Object.defineProperty(Event.prototype, 'AT_TARGET', { ++ writable: true, ++ configurable: true, + enumerable: true, + value: 2, + }); + + // $FlowExpectedError[cannot-write] + Object.defineProperty(Event, 'BUBBLING_PHASE', { ++ writable: true, ++ configurable: true, + enumerable: true, + value: 3, + }); + + // $FlowExpectedError[cannot-write] + Object.defineProperty(Event.prototype, 'BUBBLING_PHASE', { ++ writable: true, ++ configurable: true, + enumerable: true, + value: 3, + }); diff --git a/carry-your-live/src/components/ListItem.tsx b/carry-your-live/src/components/ListItem.tsx index c51a33f..ea96738 100644 --- a/carry-your-live/src/components/ListItem.tsx +++ b/carry-your-live/src/components/ListItem.tsx @@ -1,7 +1,17 @@ import React from 'react'; import { View, Text, TouchableOpacity, StyleSheet, Switch } from 'react-native'; -export const ListItem = ({ title, subtitle, onPress, leftIcon, rightIcon, disabled, leftElement, rightElement, showChevron }: { +export const ListItem = ({ + title, + subtitle, + onPress, + leftIcon, + rightIcon, + disabled, + leftElement, + rightElement, + showChevron, +}: { title: string; subtitle?: string | undefined; onPress?: () => void; @@ -66,4 +76,4 @@ const styles = StyleSheet.create({ color: '#757575', marginLeft: 4, }, -}); \ No newline at end of file +}); diff --git a/carry-your-live/src/components/OptionPickerModal.tsx b/carry-your-live/src/components/OptionPickerModal.tsx index 03140d6..7a4c5f5 100644 --- a/carry-your-live/src/components/OptionPickerModal.tsx +++ b/carry-your-live/src/components/OptionPickerModal.tsx @@ -19,10 +19,22 @@ interface OptionPickerModalProps { multiSelect?: boolean; } -export function OptionPickerModal({ visible, title, options, selectedValue, onSelect, onClose, multiSelect = false }: OptionPickerModalProps) { +export function OptionPickerModal({ + visible, + title, + options, + selectedValue, + onSelect, + onClose, + multiSelect = false, +}: OptionPickerModalProps) { const { theme } = useSettings(); - const selectedValues = Array.isArray(selectedValue) ? selectedValue : selectedValue == null ? [] : [selectedValue]; + const selectedValues = Array.isArray(selectedValue) + ? selectedValue + : selectedValue == null + ? [] + : [selectedValue]; return ( @@ -31,7 +43,7 @@ export function OptionPickerModal({ visible, title, options, selectedValue, onSe {title} item.value} + keyExtractor={item => item.value} renderItem={({ item }) => { const selected = selectedValues.includes(item.value); return ( @@ -44,7 +56,7 @@ export function OptionPickerModal({ visible, title, options, selectedValue, onSe onPress={() => { if (multiSelect) { const newValues = selected - ? selectedValues.filter((v) => v !== item.value) + ? selectedValues.filter(v => v !== item.value) : [...selectedValues, item.value]; onSelect(newValues); } else { @@ -57,23 +69,49 @@ export function OptionPickerModal({ visible, title, options, selectedValue, onSe accessibilityLabel={item.label} accessibilityState={multiSelect ? { checked: selected } : { selected }} > - {item.color && ( - - )} - + {item.color && } + {item.label} {multiSelect ? ( - + {selected && ( - + )} - ) : selected && ( - - - + ) : ( + selected && ( + + + + ) )} ); diff --git a/carry-your-live/src/components/PrioritySelector.tsx b/carry-your-live/src/components/PrioritySelector.tsx index faf928c..4f5dd9c 100644 --- a/carry-your-live/src/components/PrioritySelector.tsx +++ b/carry-your-live/src/components/PrioritySelector.tsx @@ -24,13 +24,17 @@ export function PrioritySelector({ value, onChange }: PrioritySelectorProps) { Priority - {priorities.map((priority) => ( + {priorities.map(priority => ( onChange(priority.value)} activeOpacity={0.8} @@ -38,16 +42,20 @@ export function PrioritySelector({ value, onChange }: PrioritySelectorProps) { accessibilityLabel={`${priority.label} priority`} accessibilityState={{ selected: value === priority.value }} > - - + + {priority.label} @@ -97,4 +105,4 @@ const styles = StyleSheet.create({ fontSize: 13, fontWeight: '500', }, -}); \ No newline at end of file +}); diff --git a/carry-your-live/src/components/QuickAddBar.tsx b/carry-your-live/src/components/QuickAddBar.tsx index 2ed6e37..1df9784 100644 --- a/carry-your-live/src/components/QuickAddBar.tsx +++ b/carry-your-live/src/components/QuickAddBar.tsx @@ -1,5 +1,13 @@ import React, { useState, useEffect, useMemo, useRef } from 'react'; -import { View, StyleSheet, TextInput, TouchableOpacity, Keyboard, Text, Platform } from 'react-native'; +import { + View, + StyleSheet, + TextInput, + TouchableOpacity, + Keyboard, + Text, + Platform, +} from 'react-native'; import { BlurView } from 'expo-blur'; import { database, collections } from '@/database'; import { useCategories } from '@/hooks/useDatabase'; @@ -14,10 +22,20 @@ interface QuickAddBarProps { placeholder?: string; } -const BlurContainer = ({ children, style, theme }: { children: React.ReactNode; style: any; theme: any }) => { +const BlurContainer = ({ + children, + style, + theme, +}: { + children: React.ReactNode; + style: any; + theme: any; +}) => { if (Platform.OS === 'web') { return ( - + {children} ); @@ -38,10 +56,10 @@ export function QuickAddBar({ dueDate = 0, placeholder }: QuickAddBarProps) { const inputRef = useRef(null); const visibleCategories = useMemo( - () => categories.filter((c) => c.name.toLowerCase() !== 'calendar'), + () => categories.filter(c => c.name.toLowerCase() !== 'calendar'), [categories] ); - const categoryColor = visibleCategories.find((c) => c.id === categoryId)?.color ?? theme.textMuted; + const categoryColor = visibleCategories.find(c => c.id === categoryId)?.color ?? theme.textMuted; useEffect(() => { return subscribeToQuickAdd(() => { @@ -57,7 +75,7 @@ export function QuickAddBar({ dueDate = 0, placeholder }: QuickAddBarProps) { const now = new Date(); await database.write(async () => { - await collections.tasks.create((t) => { + await collections.tasks.create(t => { t.title = trimmed; t.description = ''; t.categoryId = categoryId || ''; @@ -83,16 +101,31 @@ export function QuickAddBar({ dueDate = 0, placeholder }: QuickAddBarProps) { return ( - + setCategoryPickerVisible(true)} activeOpacity={0.7} accessibilityLabel="Select tags" > - + ({ value: c.id, label: c.name, color: c.color }))]} + options={[ + { value: '', label: 'None', color: theme.textMuted }, + ...visibleCategories.map(c => ({ value: c.id, label: c.name, color: c.color })), + ]} selectedValue={categoryId} - onSelect={(value) => setCategoryId(Array.isArray(value) ? value[0] : value)} + onSelect={value => setCategoryId(Array.isArray(value) ? value[0] : value)} onClose={() => setCategoryPickerVisible(false)} /> @@ -162,7 +202,7 @@ const styles = StyleSheet.create({ borderBottomWidth: 1, shadowColor: '#000', shadowOffset: { width: 0, height: -2 }, - shadowOpacity: 0.10, + shadowOpacity: 0.1, shadowRadius: 6, elevation: 6, minHeight: 56, @@ -208,4 +248,4 @@ const styles = StyleSheet.create({ submitDisabled: { opacity: 0.5, }, -}); \ No newline at end of file +}); diff --git a/carry-your-live/src/components/ReminderSelector.tsx b/carry-your-live/src/components/ReminderSelector.tsx index 1d955fa..5cdcb0d 100644 --- a/carry-your-live/src/components/ReminderSelector.tsx +++ b/carry-your-live/src/components/ReminderSelector.tsx @@ -18,9 +18,13 @@ export function ReminderSelector({ value, onChange, hasDueDate }: ReminderSelect const selectedReminders = parseReminders(value); const disabled = !hasDueDate; - const reminderLabels = selectedReminders.length > 0 - ? selectedReminders.map(r => REMINDER_OPTIONS.find(o => o.value === r)?.label).filter(Boolean).join(', ') - : 'No reminder'; + const reminderLabels = + selectedReminders.length > 0 + ? selectedReminders + .map(r => REMINDER_OPTIONS.find(o => o.value === r)?.label) + .filter(Boolean) + .join(', ') + : 'No reminder'; return ( @@ -29,7 +33,10 @@ export function ReminderSelector({ value, onChange, hasDueDate }: ReminderSelect style={[ styles.row, { backgroundColor: theme.card, borderColor: theme.borderStrong }, - selectedReminders.length > 0 && { borderColor: theme.accent, backgroundColor: theme.accentSoft }, + selectedReminders.length > 0 && { + borderColor: theme.accent, + backgroundColor: theme.accentSoft, + }, disabled && { opacity: 0.5 }, ]} onPress={() => setShowPicker(true)} @@ -49,7 +56,14 @@ export function ReminderSelector({ value, onChange, hasDueDate }: ReminderSelect strokeLinejoin="round" fill="none" /> - 0 ? theme.accent : 'transparent'} stroke={selectedReminders.length > 0 ? theme.accentText : theme.textSecondary} strokeWidth={1.8} /> + 0 ? theme.accent : 'transparent'} + stroke={selectedReminders.length > 0 ? theme.accentText : theme.textSecondary} + strokeWidth={1.8} + /> - + {!hasDueDate && ( - Set a due date to add reminders. + + Set a due date to add reminders. + )} o.value !== 'none').map((o) => ({ value: o.value, label: o.label }))} + options={REMINDER_OPTIONS.filter(o => o.value !== 'none').map(o => ({ + value: o.value, + label: o.label, + }))} selectedValue={selectedReminders} - onSelect={(v) => onChange(toRemindersString(v as Reminder[]))} + onSelect={v => onChange(toRemindersString(v as Reminder[]))} onClose={() => setShowPicker(false)} multiSelect /> diff --git a/carry-your-live/src/components/RepeatSelector.tsx b/carry-your-live/src/components/RepeatSelector.tsx index 6d36a16..3dde6c3 100644 --- a/carry-your-live/src/components/RepeatSelector.tsx +++ b/carry-your-live/src/components/RepeatSelector.tsx @@ -1,6 +1,22 @@ import React from 'react'; -import { View, Text, StyleSheet, TouchableOpacity, Modal, TextInput, Pressable, KeyboardAvoidingView } from 'react-native'; -import { Repeat, REPEAT_OPTIONS, WEEKDAY_LABELS, WEEKDAY_ORDER, weekdaysInDisplayOrder, repeatDaysFromString } from '@/types'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + Modal, + TextInput, + Pressable, + KeyboardAvoidingView, +} from 'react-native'; +import { + Repeat, + REPEAT_OPTIONS, + WEEKDAY_LABELS, + WEEKDAY_ORDER, + weekdaysInDisplayOrder, + repeatDaysFromString, +} from '@/types'; import { useSettings } from '@/theme'; import { useRepeatProfiles } from '@/hooks/useDatabase'; import { createRepeatProfile, deleteRepeatProfile } from '@/utils/repeatProfileActions'; @@ -43,7 +59,15 @@ function RepeatIcon({ repeat, color }: { repeat: Repeat; color: string }) { ); } -const WEEKDAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; +const WEEKDAY_NAMES = [ + 'Sunday', + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', +]; function unitLabel(value: Repeat): string { switch (value) { @@ -77,12 +101,16 @@ export function RepeatSelector({ value, interval, days, onChange }: RepeatSelect }; const toggleDay = (day: number) => { - const nextDays = days.includes(day) ? days.filter((d) => d !== day) : [...days, day].sort(); + const nextDays = days.includes(day) ? days.filter(d => d !== day) : [...days, day].sort(); if (value === 'custom' && nextDays.length === 0) return; onChange(value, interval, nextDays); }; - const applyProfile = (profile: { repeat: Repeat; repeatInterval: number; repeatDays: string }) => { + const applyProfile = (profile: { + repeat: Repeat; + repeatInterval: number; + repeatDays: string; + }) => { onChange(profile.repeat, profile.repeatInterval, repeatDaysFromString(profile.repeatDays)); }; @@ -116,7 +144,7 @@ export function RepeatSelector({ value, interval, days, onChange }: RepeatSelect Profiles - {profiles.map((profile) => ( + {profiles.map(profile => ( - applyProfile(profile)} activeOpacity={0.8} accessibilityRole="button" accessibilityLabel={`Apply repeat profile ${profile.name}`}> - {profile.name} + applyProfile(profile)} + activeOpacity={0.8} + accessibilityRole="button" + accessibilityLabel={`Apply repeat profile ${profile.name}`} + > + + {profile.name} + handleDeleteProfile(profile.id, profile.name)} @@ -149,13 +184,17 @@ export function RepeatSelector({ value, interval, days, onChange }: RepeatSelect )} - {REPEAT_OPTIONS.map((option) => ( + {REPEAT_OPTIONS.map(option => ( selectRepeat(option.value)} activeOpacity={0.8} @@ -163,7 +202,10 @@ export function RepeatSelector({ value, interval, days, onChange }: RepeatSelect accessibilityLabel={`Repeat ${option.label.replace('No Repeat', 'none')}`} accessibilityState={{ selected: value === option.value }} > - + - + Every - − + + − + {interval} = 30 }} > - = 30 ? theme.textMuted : theme.text }]}>+ + = 30 ? theme.textMuted : theme.text }, + ]} + > + + + {unitLabel(value)} @@ -212,14 +273,20 @@ export function RepeatSelector({ value, interval, days, onChange }: RepeatSelect {isDaysBased(value) && ( - {WEEKDAY_ORDER.map((day) => ( + {WEEKDAY_ORDER.map(day => ( toggleDay(day)} activeOpacity={0.8} @@ -250,9 +317,16 @@ export function RepeatSelector({ value, interval, days, onChange }: RepeatSelect activeOpacity={0.8} > - + - Save as profile + + Save as profile + )} @@ -262,19 +336,29 @@ export function RepeatSelector({ value, interval, days, onChange }: RepeatSelect animationType="fade" onRequestClose={() => setSaveModalVisible(false)} > - + setSaveModalVisible(false)}> - + Save repeat profile - {`${REPEAT_OPTIONS.find((o) => o.value === value)?.label.replace('No Repeat', 'None')}, every ${interval} ${unitLabel(value)}${interval > 1 ? 's' : ''}`} - {isDaysBased(value) && days.length > 0 ? ` · ${weekdaysInDisplayOrder(days).map((d) => WEEKDAY_LABELS[d]).join(' ')}` : ''} + {`${REPEAT_OPTIONS.find(o => o.value === value)?.label.replace('No Repeat', 'None')}, every ${interval} ${unitLabel(value)}${interval > 1 ? 's' : ''}`} + {isDaysBased(value) && days.length > 0 + ? ` · ${weekdaysInDisplayOrder(days) + .map(d => WEEKDAY_LABELS[d]) + .join(' ')}` + : ''} setSaveModalVisible(false)} activeOpacity={0.8} > - Cancel + + Cancel + - Save + + Save + diff --git a/carry-your-live/src/components/ServerUrlModal.tsx b/carry-your-live/src/components/ServerUrlModal.tsx index 1be9ff3..99d1314 100644 --- a/carry-your-live/src/components/ServerUrlModal.tsx +++ b/carry-your-live/src/components/ServerUrlModal.tsx @@ -1,5 +1,13 @@ import React, { useState } from 'react'; -import { Modal, View, Text, StyleSheet, TextInput, TouchableOpacity, KeyboardAvoidingView } from 'react-native'; +import { + Modal, + View, + Text, + StyleSheet, + TextInput, + TouchableOpacity, + KeyboardAvoidingView, +} from 'react-native'; import { useSettings } from '@/theme'; import { DEFAULT_API_BASE_URL } from '@/services/auth'; import Svg, { Path } from 'react-native-svg'; @@ -33,16 +41,22 @@ export function ServerUrlModal({ visible, onClose }: ServerUrlModalProps) { return ( - + Backend URL - + - + @@ -51,7 +65,11 @@ export function ServerUrlModal({ visible, onClose }: ServerUrlModalProps) { Cancel 0; const hovered = hoveredId === subtask.id; const effectiveColor = subtask.categoryId - ? categoryColorResolver?.(subtask.categoryId) ?? categoryColor + ? (categoryColorResolver?.(subtask.categoryId) ?? categoryColor) : categoryColor; const handleExpand = () => setExpanded(!expanded); @@ -59,7 +59,7 @@ export const SubtaskItem = React.memo(function SubtaskItem({ return ( - registerRef?.(subtask.id, subtask.taskId, ref)}> + registerRef?.(subtask.id, subtask.taskId, ref)}> a.order - b.order) - .map((child) => ( + .map(child => ( Subtasks {items.length > 0 && ( - {items.length} + + {items.length} + )} @@ -92,20 +94,30 @@ export function SubtasksSection({ control }: SubtasksSectionProps) { key={item._key ?? `subtask-${index}`} index={index} value={item.title} - onChange={(value) => updateSubtask(index, value)} + onChange={value => updateSubtask(index, value)} onRemove={() => removeSubtask(index)} /> ))} - + - Add Subtask + + Add Subtask + @@ -150,7 +162,12 @@ function SubtaskItem({ index, value, onChange, onRemove }: SubtaskItemProps) { /> - + @@ -234,4 +251,4 @@ const styles = StyleSheet.create({ fontSize: 12, fontWeight: '500', }, -}); \ No newline at end of file +}); diff --git a/carry-your-live/src/components/SyncModal.tsx b/carry-your-live/src/components/SyncModal.tsx index 93c0546..1244772 100644 --- a/carry-your-live/src/components/SyncModal.tsx +++ b/carry-your-live/src/components/SyncModal.tsx @@ -11,9 +11,18 @@ import { } from 'react-native'; import { useSettings } from '@/theme'; import { AuthUser, getAuthToken, getAuthUser, login, register, signOutAuth } from '@/services/auth'; -import { runSyncGuarded, startAutoSync, stopAutoSync, getLastSyncTime, SyncResult } from '@/database/sync'; +import { + runSyncGuarded, + startAutoSync, + stopAutoSync, + getLastSyncTime, + SyncResult, +} from '@/database/sync'; import { ForgotPasswordModal } from './ForgotPasswordModal'; -import { registerForPushNotificationsAsync, unregisterForPushNotificationsAsync } from '@/services/pushNotifications'; +import { + registerForPushNotificationsAsync, + unregisterForPushNotificationsAsync, +} from '@/services/pushNotifications'; interface SyncModalProps { visible: boolean; @@ -81,9 +90,10 @@ export function SyncModal({ visible, onClose }: SyncModalProps) { } setBusy(true); try { - const authedUser = mode === 'register' - ? await register(username.trim(), password) - : await login(username.trim(), password); + const authedUser = + mode === 'register' + ? await register(username.trim(), password) + : await login(username.trim(), password); setUser(authedUser); startAutoSync(); registerForPushNotificationsAsync().catch(() => {}); @@ -128,10 +138,7 @@ export function SyncModal({ visible, onClose }: SyncModalProps) { return ( - + @@ -159,7 +166,9 @@ export function SyncModal({ visible, onClose }: SyncModalProps) { {status === 'syncing' ? ( ) : ( - Sync Now + + Sync Now + )} @@ -181,7 +190,9 @@ export function SyncModal({ visible, onClose }: SyncModalProps) { onPress={handleSignOut} activeOpacity={0.7} > - Sign Out + + Sign Out + ) : ( @@ -191,13 +202,10 @@ export function SyncModal({ visible, onClose }: SyncModalProps) { - {(['login', 'register'] as const).map((m) => ( + {(['login', 'register'] as const).map(m => ( { setMode(m); setError(null); @@ -222,7 +230,14 @@ export function SyncModal({ visible, onClose }: SyncModalProps) { )} setShowForgotPassword(true)} activeOpacity={0.7} > - Forgot Password? + + Forgot Password? + )} - {error && {error}} + {error && ( + {error} + )} )} - setShowForgotPassword(false)} /> + setShowForgotPassword(false)} + /> - + ); case 'error': return ( - + ); default: return ( - + ); @@ -88,16 +105,25 @@ export default function SyncStatus({ compact = false }: SyncStatusProps) { const getStatusText = () => { switch (status) { - case 'syncing': return 'Syncing...'; - case 'success': return 'Synced'; - case 'error': return 'Sync failed'; - default: return lastSync ? `Last sync: ${formatTime(lastSync)}` : 'Not synced'; + case 'syncing': + return 'Syncing...'; + case 'success': + return 'Synced'; + case 'error': + return 'Sync failed'; + default: + return lastSync ? `Last sync: ${formatTime(lastSync)}` : 'Not synced'; } }; if (compact) { return ( - + {getStatusIcon()} @@ -116,12 +142,20 @@ export default function SyncStatus({ compact = false }: SyncStatusProps) { {getStatusText()} - + {status === 'syncing' ? 'Syncing...' : 'Sync Now'} @@ -193,4 +227,4 @@ const styles = StyleSheet.create({ compactText: { fontSize: 12, }, -}); \ No newline at end of file +}); diff --git a/carry-your-live/src/components/TabBarIcon.tsx b/carry-your-live/src/components/TabBarIcon.tsx index c0c147a..7a09584 100644 --- a/carry-your-live/src/components/TabBarIcon.tsx +++ b/carry-your-live/src/components/TabBarIcon.tsx @@ -56,7 +56,14 @@ export function TabBarIcon({ name, focused, color, size = 24 }: TabBarIconProps) )} {name === 'gear' && ( <> - + - {focused && ( - - )} + {focused && } )} ); -} \ No newline at end of file +} diff --git a/carry-your-live/src/components/TaskDeleteModal.tsx b/carry-your-live/src/components/TaskDeleteModal.tsx index a0b30e3..489790b 100644 --- a/carry-your-live/src/components/TaskDeleteModal.tsx +++ b/carry-your-live/src/components/TaskDeleteModal.tsx @@ -14,7 +14,15 @@ interface TaskDeleteModalProps { onDelete: (scope: TaskDeleteScope) => void; } -export function TaskDeleteModal({ visible, taskId, taskTitle, isRepeating, subtask, onClose, onDelete }: TaskDeleteModalProps) { +export function TaskDeleteModal({ + visible, + taskId, + taskTitle, + isRepeating, + subtask, + onClose, + onDelete, +}: TaskDeleteModalProps) { const { theme } = useSettings(); const [counts, setCounts] = useState({ future: 1, all: 1 }); @@ -22,11 +30,13 @@ export function TaskDeleteModal({ visible, taskId, taskTitle, isRepeating, subta if (!visible || !taskId || subtask) return; let mounted = true; getSeriesOccurrenceCounts(taskId) - .then((c) => { + .then(c => { if (mounted) setCounts(c); }) .catch(() => {}); - return () => { mounted = false; }; + return () => { + mounted = false; + }; }, [visible, taskId, subtask]); const options: { scope: TaskDeleteScope; label: string; hint?: string }[] = subtask @@ -34,7 +44,11 @@ export function TaskDeleteModal({ visible, taskId, taskTitle, isRepeating, subta : [{ scope: 'this', label: 'This task only' }]; if (isRepeating) { - options.push({ scope: 'future', label: 'This and future tasks', hint: counts.future > 1 ? `${counts.future} occurrences` : undefined }); + options.push({ + scope: 'future', + label: 'This and future tasks', + hint: counts.future > 1 ? `${counts.future} occurrences` : undefined, + }); options.push({ scope: 'all', label: 'All tasks in series', hint: `${counts.all} occurrences` }); } @@ -43,10 +57,21 @@ export function TaskDeleteModal({ visible, taskId, taskTitle, isRepeating, subta - {subtask ? 'Delete Subtask' : 'Delete Task'} - + + {subtask ? 'Delete Subtask' : 'Delete Task'} + + - + @@ -64,19 +89,26 @@ export function TaskDeleteModal({ visible, taskId, taskTitle, isRepeating, subta )} - {options.map((option) => ( + {options.map(option => ( onDelete(option.scope)} activeOpacity={0.7} > - {option.label} - {option.hint && {option.hint}} + + {option.label} + + {option.hint && ( + {option.hint} + )} ))} diff --git a/carry-your-live/src/components/TaskItem.tsx b/carry-your-live/src/components/TaskItem.tsx index 690753f..c340682 100644 --- a/carry-your-live/src/components/TaskItem.tsx +++ b/carry-your-live/src/components/TaskItem.tsx @@ -1,6 +1,19 @@ import React from 'react'; -import { View, Text, StyleSheet, TouchableOpacity, Animated, Platform, Dimensions } from 'react-native'; -import { Swipeable, Gesture, GestureDetector, PanGestureHandler } from 'react-native-gesture-handler'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + Animated, + Platform, + Dimensions, +} from 'react-native'; +import { + Swipeable, + Gesture, + GestureDetector, + PanGestureHandler, +} from 'react-native-gesture-handler'; import { TaskData, SubtaskData, Priority, Repeat, Reminder } from '@/types'; import { PRIORITY_COLORS } from '@/constants'; import { useSettings, mixHex } from '@/theme'; @@ -46,7 +59,30 @@ interface TaskItemProps { categoryColors?: (string | undefined)[]; } -export const TaskItem = React.memo(function TaskItem({ task, onToggle, onDelete, onPress, onLongPress, onMenuOpen, selected, selectionMode, completedSection, draggable, hovered, onDragStart, onDragUpdate, onDragEnd, onReorderStart, onReorderUpdate, onReorderEnd, expanded, indented, depth = 0, categoryColor, categoryColors }: TaskItemProps) { +export const TaskItem = React.memo(function TaskItem({ + task, + onToggle, + onDelete, + onPress, + onLongPress, + onMenuOpen, + selected, + selectionMode, + completedSection, + draggable, + hovered, + onDragStart, + onDragUpdate, + onDragEnd, + onReorderStart, + onReorderUpdate, + onReorderEnd, + expanded, + indented, + depth = 0, + categoryColor, + categoryColors, +}: TaskItemProps) { const { theme, deleteBySwipe } = useSettings(); const styles = React.useMemo(() => createStyles(theme), [theme]); const [opacityAnim] = React.useState(new Animated.Value(task.completed ? 0.5 : 1)); @@ -73,17 +109,17 @@ export const TaskItem = React.memo(function TaskItem({ task, onToggle, onDelete, .activateAfterLongPress(400) .minDistance(5) .runOnJS(true) - .onStart((e) => { + .onStart(e => { setDragging(true); setDragStartY(e.absoluteY); onReorderStart?.(); }) - .onUpdate((e) => { + .onUpdate(e => { dragTranslateX.setValue(e.translationX); dragTranslateY.setValue(e.translationY); onReorderUpdate?.(e.absoluteY); }) - .onEnd((e) => { + .onEnd(e => { onReorderEnd?.(e.absoluteY, e.translationY); }) .onFinalize(() => { @@ -104,12 +140,12 @@ export const TaskItem = React.memo(function TaskItem({ task, onToggle, onDelete, setDragging(true); onDragStart?.(); }) - .onUpdate((e) => { + .onUpdate(e => { dragTranslateX.setValue(e.translationX); dragTranslateY.setValue(e.translationY); onDragUpdate?.(e.absoluteY); }) - .onEnd((e) => { + .onEnd(e => { onDragEnd?.(e.absoluteY); }) .onFinalize(() => { @@ -135,7 +171,11 @@ export const TaskItem = React.memo(function TaskItem({ task, onToggle, onDelete, endOfToday.setHours(23, 59, 59, 999); const hasDueDate = task.dueDate > 0; const isOverdue = hasDueDate && !task.completed && task.dueDate < startOfToday.getTime(); - const isDueToday = hasDueDate && !task.completed && task.dueDate >= startOfToday.getTime() && task.dueDate <= endOfToday.getTime(); + const isDueToday = + hasDueDate && + !task.completed && + task.dueDate >= startOfToday.getTime() && + task.dueDate <= endOfToday.getTime(); return { hasDueDate, isOverdue, isDueToday }; }, [task.dueDate, task.completed]); @@ -149,21 +189,39 @@ export const TaskItem = React.memo(function TaskItem({ task, onToggle, onDelete, const renderRightActions = (progress: Animated.AnimatedInterpolation) => { const translateX = progress.interpolate({ inputRange: [0, 1], outputRange: [80, 0] }); return ( - + - + Delete ); }; -const renderLeftActions = (progress: Animated.AnimatedInterpolation) => { + const renderLeftActions = (progress: Animated.AnimatedInterpolation) => { const translateX = progress.interpolate({ inputRange: [0, 1], outputRange: [-80, 0] }); return ( - + - + {task.completed ? 'Undo' : 'Done'} @@ -188,7 +246,9 @@ const renderLeftActions = (progress: Animated.AnimatedInterpolation) => overshootRight={false} overshootLeft={false} > - + ) => ]} pointerEvents={dragging ? 'none' : 'auto'} > - - + + {categoryColor && ( @@ -236,10 +296,24 @@ const renderLeftActions = (progress: Animated.AnimatedInterpolation) => {task.completed ? ( <> - + ) : ( - + )} @@ -253,90 +327,30 @@ const renderLeftActions = (progress: Animated.AnimatedInterpolation) => > {task.title} - {task.priority !== 'none' && ( - - - {task.priority.charAt(0).toUpperCase()} - - - )} - {task.assigneeId && ( - - - {task.assigneeId.charAt(0).toUpperCase()} - - - )} - {task.repeat && task.repeat !== 'none' && ( - - - - - - )} - {task.allDay && ( - - - - - - All Day - - )} - - - - - - - {hasDueDate && ( - - - - - - - {formattedDueDate} - - {task.reminder && task.reminder !== 'none' && ( - - + {task.priority !== 'none' && ( + + {task.priority.charAt(0).toUpperCase()} + + )} + {task.assigneeId && ( + + + {task.assigneeId.charAt(0).toUpperCase()} + + + )} + {task.repeat && task.repeat !== 'none' && ( + + ) => )} + {task.allDay && ( + + + + + + All Day + + )} + + + + + - )} - - + {hasDueDate && ( + + + + + + + {formattedDueDate} + + {task.reminder && task.reminder !== 'none' && ( + + + + + + )} + + )} + + - - - - - - - + + + + + + + @@ -400,204 +491,205 @@ function formatTime12(time: string): string { return `${h}:${String(minutes).padStart(2, '0')} ${period}`; } -const createStyles = (theme: ThemeColors) => StyleSheet.create({ - container: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: 12, - paddingVertical: 14, - borderRadius: 16, - borderWidth: 1, - marginVertical: 4, - shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.05, - shadowRadius: 6, - elevation: 1, - }, - taskCompleted: { - opacity: 0.5, - }, - indented: { - marginLeft: 32, - marginBottom: 4, - }, - taskOverdue: { - borderColor: theme.dangerBorder, - }, - taskDueToday: { - borderColor: mixHex(theme.info, theme.background, 0.55), - }, - taskSelected: { - shadowColor: '#000', - shadowOpacity: 0.1, - shadowRadius: 6, - elevation: 2, - }, - taskHovered: { - borderColor: mixHex(theme.info, theme.background, 0.55), - backgroundColor: mixHex(theme.info, theme.background, 0.85), - }, - dragLifted: { - zIndex: 100, - elevation: 12, - shadowColor: '#000', - shadowOpacity: 0.25, - shadowRadius: 12, - shadowOffset: { width: 0, height: 8 }, - }, - contentWrapper: { - flex: 1, - }, - content: { - gap: 12, - }, - titleRow: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - }, - dragHandle: { - width: 28, - height: 28, - borderRadius: 14, - alignItems: 'center', - justifyContent: 'center', - marginRight: 8, - }, - categoryDotSlot: { - width: 28, - height: 28, - alignItems: 'center', - justifyContent: 'center', - marginRight: 8, - }, - categoryDot: { - width: 12, - height: 12, - borderRadius: 6, - }, - title: { - fontSize: 17, - fontWeight: '500', - flex: 1, - marginRight: 16, - }, - titleCompleted: { - textDecorationLine: 'line-through', - color: theme.textMuted, - }, - repeatIcon: { - marginLeft: 8, - width: 14, - height: 14, - alignItems: 'center', - justifyContent: 'center', - }, - chevron: { - marginLeft: 8, - width: 16, - height: 16, - alignItems: 'center', - justifyContent: 'center', - }, - reminderIcon: { - marginLeft: 6, - width: 13, - height: 13, - alignItems: 'center', - justifyContent: 'center', - }, - titleOverdue: { - color: theme.danger, - }, - priorityBadge: { - paddingHorizontal: 6, - paddingVertical: 2, - borderRadius: 8, - minWidth: 20, - alignItems: 'center', - }, - priorityText: { - fontSize: 9, - fontWeight: '700', - color: '#FFFFFF', - }, - assigneeBadge: { - width: 20, - height: 20, - borderRadius: 10, - backgroundColor: theme.dangerSoft, - alignItems: 'center', - justifyContent: 'center', - }, - assigneeText: { - fontSize: 9, - fontWeight: '700', - color: theme.danger, - }, - dueRow: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, - }, - dueText: { - fontSize: 13, - }, - dueTextOverdue: { - color: theme.danger, - fontWeight: '600', - }, - dueTextDueToday: { - color: theme.info, - fontWeight: '600', - }, - menuButton: { - padding: 4, - marginLeft: 8, - }, - allDayBadge: { - flexDirection: 'row', - alignItems: 'center', - gap: 4, - marginLeft: 8, - paddingHorizontal: 6, - paddingVertical: 2, - borderRadius: 8, - backgroundColor: mixHex(theme.info, theme.background, 0.85), - }, - allDayText: { - fontSize: 11, - fontWeight: '600', - color: theme.info, - }, - checkCircle: { - width: 30, - height: 30, - borderRadius: 15, - alignItems: 'center', - justifyContent: 'center', - marginRight: 12, - }, - swipeAction: { - width: 80, - alignItems: 'center', - justifyContent: 'center', - gap: 4, - }, - swipeDelete: { - backgroundColor: theme.dangerStrong, - borderTopRightRadius: 16, - borderBottomRightRadius: 16, - }, - swipeComplete: { - backgroundColor: theme.successStrong, - borderTopLeftRadius: 16, - borderBottomLeftRadius: 16, - }, - swipeActionText: { - color: '#FFFFFF', - fontSize: 12, - fontWeight: '600', - }, -}); +const createStyles = (theme: ThemeColors) => + StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 12, + paddingVertical: 14, + borderRadius: 16, + borderWidth: 1, + marginVertical: 4, + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.05, + shadowRadius: 6, + elevation: 1, + }, + taskCompleted: { + opacity: 0.5, + }, + indented: { + marginLeft: 32, + marginBottom: 4, + }, + taskOverdue: { + borderColor: theme.dangerBorder, + }, + taskDueToday: { + borderColor: mixHex(theme.info, theme.background, 0.55), + }, + taskSelected: { + shadowColor: '#000', + shadowOpacity: 0.1, + shadowRadius: 6, + elevation: 2, + }, + taskHovered: { + borderColor: mixHex(theme.info, theme.background, 0.55), + backgroundColor: mixHex(theme.info, theme.background, 0.85), + }, + dragLifted: { + zIndex: 100, + elevation: 12, + shadowColor: '#000', + shadowOpacity: 0.25, + shadowRadius: 12, + shadowOffset: { width: 0, height: 8 }, + }, + contentWrapper: { + flex: 1, + }, + content: { + gap: 12, + }, + titleRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + dragHandle: { + width: 28, + height: 28, + borderRadius: 14, + alignItems: 'center', + justifyContent: 'center', + marginRight: 8, + }, + categoryDotSlot: { + width: 28, + height: 28, + alignItems: 'center', + justifyContent: 'center', + marginRight: 8, + }, + categoryDot: { + width: 12, + height: 12, + borderRadius: 6, + }, + title: { + fontSize: 17, + fontWeight: '500', + flex: 1, + marginRight: 16, + }, + titleCompleted: { + textDecorationLine: 'line-through', + color: theme.textMuted, + }, + repeatIcon: { + marginLeft: 8, + width: 14, + height: 14, + alignItems: 'center', + justifyContent: 'center', + }, + chevron: { + marginLeft: 8, + width: 16, + height: 16, + alignItems: 'center', + justifyContent: 'center', + }, + reminderIcon: { + marginLeft: 6, + width: 13, + height: 13, + alignItems: 'center', + justifyContent: 'center', + }, + titleOverdue: { + color: theme.danger, + }, + priorityBadge: { + paddingHorizontal: 6, + paddingVertical: 2, + borderRadius: 8, + minWidth: 20, + alignItems: 'center', + }, + priorityText: { + fontSize: 9, + fontWeight: '700', + color: '#FFFFFF', + }, + assigneeBadge: { + width: 20, + height: 20, + borderRadius: 10, + backgroundColor: theme.dangerSoft, + alignItems: 'center', + justifyContent: 'center', + }, + assigneeText: { + fontSize: 9, + fontWeight: '700', + color: theme.danger, + }, + dueRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + dueText: { + fontSize: 13, + }, + dueTextOverdue: { + color: theme.danger, + fontWeight: '600', + }, + dueTextDueToday: { + color: theme.info, + fontWeight: '600', + }, + menuButton: { + padding: 4, + marginLeft: 8, + }, + allDayBadge: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + marginLeft: 8, + paddingHorizontal: 6, + paddingVertical: 2, + borderRadius: 8, + backgroundColor: mixHex(theme.info, theme.background, 0.85), + }, + allDayText: { + fontSize: 11, + fontWeight: '600', + color: theme.info, + }, + checkCircle: { + width: 30, + height: 30, + borderRadius: 15, + alignItems: 'center', + justifyContent: 'center', + marginRight: 12, + }, + swipeAction: { + width: 80, + alignItems: 'center', + justifyContent: 'center', + gap: 4, + }, + swipeDelete: { + backgroundColor: theme.dangerStrong, + borderTopRightRadius: 16, + borderBottomRightRadius: 16, + }, + swipeComplete: { + backgroundColor: theme.successStrong, + borderTopLeftRadius: 16, + borderBottomLeftRadius: 16, + }, + swipeActionText: { + color: '#FFFFFF', + fontSize: 12, + fontWeight: '600', + }, + }); diff --git a/carry-your-live/src/components/TaskList.tsx b/carry-your-live/src/components/TaskList.tsx index 77b0a5b..bdd566b 100644 --- a/carry-your-live/src/components/TaskList.tsx +++ b/carry-your-live/src/components/TaskList.tsx @@ -1,5 +1,13 @@ import React, { useState, useCallback, useMemo, useRef } from 'react'; -import { View, Text, StyleSheet, Animated, RefreshControl, Alert, TouchableOpacity } from 'react-native'; +import { + View, + Text, + StyleSheet, + Animated, + RefreshControl, + Alert, + TouchableOpacity, +} from 'react-native'; import { useTasks } from '@/hooks/useTasks'; import { useSubtasks } from '@/hooks/useSubtasks'; import { useCategories } from '@/hooks/useDatabase'; @@ -42,10 +50,18 @@ const DropIndicator = ({ theme }: { theme: any }) => ( ); -export function TaskList({ categoryIds = [], showCompleted = false, onSelectionChange }: TaskListProps) { +export function TaskList({ + categoryIds = [], + showCompleted = false, + onSelectionChange, +}: TaskListProps) { const { theme, sortBy, todoAheadDays } = useSettings(); - const { tasks, loading, refresh: refreshTasks } = useTasks(categoryIds, showCompleted ? 'all' : false, todoAheadDays); + const { + tasks, + loading, + refresh: refreshTasks, + } = useTasks(categoryIds, showCompleted ? 'all' : false, todoAheadDays); const categories = useCategories(); const categoryColors = useMemo(() => { @@ -62,19 +78,30 @@ export function TaskList({ categoryIds = [], showCompleted = false, onSelectionC const [hoverTaskId, setHoverTaskId] = useState(null); const [expandedTasks, setExpandedTasks] = useState>(new Set()); const { map: subtasksMap, refresh: refreshSubtasks } = useSubtasks(); - const [dropIndicator, setDropIndicator] = useState<{ targetId: string | null; position: 'above' | 'below' } | null>(null); + const [dropIndicator, setDropIndicator] = useState<{ + targetId: string | null; + position: 'above' | 'below'; + } | null>(null); const itemRefs = useRef>(new Map()); const subtaskRefs = useRef>(new Map()); - const dragStateRef = useRef<{ taskId: string; positions: Record; subPositions: Record } | null>(null); - const subtaskDragRef = useRef<{ subtaskId: string; parentTaskId: string; parentSubtaskId: string | null; positions: Record; subPositions: Record } | null>(null); - const taskDragRef = useRef<{ taskId: string; positions: Record } | null>(null); - const { - modals, - openTaskDelete, - openSubtaskDelete, - openSubtaskEdit, - openTaskEdit, - } = useTaskModals(); + const dragStateRef = useRef<{ + taskId: string; + positions: Record; + subPositions: Record; + } | null>(null); + const subtaskDragRef = useRef<{ + subtaskId: string; + parentTaskId: string; + parentSubtaskId: string | null; + positions: Record; + subPositions: Record; + } | null>(null); + const taskDragRef = useRef<{ + taskId: string; + positions: Record; + } | null>(null); + const { modals, openTaskDelete, openSubtaskDelete, openSubtaskEdit, openTaskEdit } = + useTaskModals(); const registerRef = useCallback((taskId: string, ref: View | null) => { if (ref) { @@ -84,13 +111,16 @@ export function TaskList({ categoryIds = [], showCompleted = false, onSelectionC } }, []); - const registerSubtaskRef = useCallback((subtaskId: string, parentTaskId: string, ref: View | null) => { - if (ref) { - subtaskRefs.current.set(subtaskId, { ref, parentTaskId }); - } else { - subtaskRefs.current.delete(subtaskId); - } - }, []); + const registerSubtaskRef = useCallback( + (subtaskId: string, parentTaskId: string, ref: View | null) => { + if (ref) { + subtaskRefs.current.set(subtaskId, { ref, parentTaskId }); + } else { + subtaskRefs.current.delete(subtaskId); + } + }, + [] + ); const categoryColorResolver = useCallback( (categoryId: string | undefined) => (categoryId ? categoryColors.get(categoryId) : undefined), @@ -101,7 +131,9 @@ export function TaskList({ categoryIds = [], showCompleted = false, onSelectionC const sorted = [...tasks]; switch (sortBy) { case 'date': - sorted.sort((a, b) => (a.dueDate || Number.MAX_SAFE_INTEGER) - (b.dueDate || Number.MAX_SAFE_INTEGER)); + sorted.sort( + (a, b) => (a.dueDate || Number.MAX_SAFE_INTEGER) - (b.dueDate || Number.MAX_SAFE_INTEGER) + ); break; case 'priority': sorted.sort((a, b) => PRIORITY_RANK[b.priority] - PRIORITY_RANK[a.priority]); @@ -146,34 +178,43 @@ export function TaskList({ categoryIds = [], showCompleted = false, onSelectionC onSelectionChange?.(false); }, [onSelectionChange]); - const enterSelection = useCallback((taskId: string) => { - setSelectionMode(true); - setSelectedIds(new Set([taskId])); - onSelectionChange?.(true); - }, [onSelectionChange]); + const enterSelection = useCallback( + (taskId: string) => { + setSelectionMode(true); + setSelectedIds(new Set([taskId])); + onSelectionChange?.(true); + }, + [onSelectionChange] + ); - const toggleSelect = useCallback((taskId: string) => { - setSelectedIds((prev) => { - const next = new Set(prev); - if (next.has(taskId)) { - next.delete(taskId); - } else { - next.add(taskId); - } - if (next.size === 0) { - setSelectionMode(false); - onSelectionChange?.(false); - } - return next; - }); - }, [onSelectionChange]); + const toggleSelect = useCallback( + (taskId: string) => { + setSelectedIds(prev => { + const next = new Set(prev); + if (next.has(taskId)) { + next.delete(taskId); + } else { + next.add(taskId); + } + if (next.size === 0) { + setSelectionMode(false); + onSelectionChange?.(false); + } + return next; + }); + }, + [onSelectionChange] + ); - const handleToggle = useCallback(async (taskId: string) => { - await confirmAndToggleTaskComplete(taskId, () => refreshTasks()); - }, [refreshTasks]); + const handleToggle = useCallback( + async (taskId: string) => { + await confirmAndToggleTaskComplete(taskId, () => refreshTasks()); + }, + [refreshTasks] + ); const toggleExpand = useCallback(async (taskId: string) => { - setExpandedTasks((prev) => { + setExpandedTasks(prev => { const next = new Set(prev); if (next.has(taskId)) { next.delete(taskId); @@ -184,24 +225,31 @@ export function TaskList({ categoryIds = [], showCompleted = false, onSelectionC }); }, []); - const handleSubtaskToggle = useCallback(async (subtaskId: string) => { - await toggleSubtaskComplete(subtaskId); - refreshTasks(); - refreshSubtasks(); - }, [refreshTasks, refreshSubtasks]); + const handleSubtaskToggle = useCallback( + async (subtaskId: string) => { + await toggleSubtaskComplete(subtaskId); + refreshTasks(); + refreshSubtasks(); + }, + [refreshTasks, refreshSubtasks] + ); const handleBulkDelete = useCallback(() => { - Alert.alert(`Delete ${selectedIds.size} task${selectedIds.size > 1 ? 's' : ''}?`, 'This cannot be undone.', [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Delete', - style: 'destructive', - onPress: async () => { - await Promise.all(Array.from(selectedIds).map(taskId => deleteTask(taskId))); - exitSelection(); + Alert.alert( + `Delete ${selectedIds.size} task${selectedIds.size > 1 ? 's' : ''}?`, + 'This cannot be undone.', + [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Delete', + style: 'destructive', + onPress: async () => { + await Promise.all(Array.from(selectedIds).map(taskId => deleteTask(taskId))); + exitSelection(); + }, }, - }, - ]); + ] + ); }, [selectedIds, exitSelection]); const handleBulkComplete = useCallback(async () => { @@ -212,14 +260,16 @@ export function TaskList({ categoryIds = [], showCompleted = false, onSelectionC const measureItems = useCallback(async () => { const positions: Record = {}; const entries = Array.from(itemRefs.current.entries()); - await Promise.all(entries.map(([id, ref]) => { - return new Promise((resolve) => { - ref?.measureInWindow((_x, y, _w, h) => { - positions[id] = { top: y, bottom: y + h }; - resolve(); + await Promise.all( + entries.map(([id, ref]) => { + return new Promise(resolve => { + ref?.measureInWindow((_x, y, _w, h) => { + positions[id] = { top: y, bottom: y + h }; + resolve(); + }); }); - }); - })); + }) + ); return positions; }, []); @@ -227,64 +277,91 @@ export function TaskList({ categoryIds = [], showCompleted = false, onSelectionC const positions = await measureItems(); const subPositions: Record = {}; const entries = Array.from(subtaskRefs.current.entries()); - await Promise.all(entries.map(([id, entry]) => { - return new Promise((resolve) => { - entry.ref?.measureInWindow((_x, y, _w, h) => { - subPositions[id] = { top: y, bottom: y + h }; - resolve(); + await Promise.all( + entries.map(([id, entry]) => { + return new Promise(resolve => { + entry.ref?.measureInWindow((_x, y, _w, h) => { + subPositions[id] = { top: y, bottom: y + h }; + resolve(); + }); }); - }); - })); + }) + ); return { positions, subPositions }; }, [measureItems]); - const findHoverTask = useCallback((absoluteY: number, draggedId: string, positions: Record) => { - const entries = Object.entries(positions); - for (let i = entries.length - 1; i >= 0; i--) { - const [id, p] = entries[i]; - if (id === draggedId) continue; - if (absoluteY >= p.top && absoluteY <= p.bottom) { - return id; + const findHoverTask = useCallback( + ( + absoluteY: number, + draggedId: string, + positions: Record + ) => { + const entries = Object.entries(positions); + for (let i = entries.length - 1; i >= 0; i--) { + const [id, p] = entries[i]; + if (id === draggedId) continue; + if (absoluteY >= p.top && absoluteY <= p.bottom) { + return id; + } } - } - return null; - }, []); + return null; + }, + [] + ); const updateHoverTaskTarget = useCallback((target: string | null) => { setHoverTaskId(target ?? null); }, []); - const findHoverTarget = useCallback((absoluteY: number, draggedId: string, positions: Record, subPositions: Record) => { - const subEntries = Object.entries(subPositions); - for (let i = subEntries.length - 1; i >= 0; i--) { - const [id, p] = subEntries[i]; - if (id === draggedId) continue; - if (absoluteY >= p.top && absoluteY <= p.bottom) { - return id; + const findHoverTarget = useCallback( + ( + absoluteY: number, + draggedId: string, + positions: Record, + subPositions: Record + ) => { + const subEntries = Object.entries(subPositions); + for (let i = subEntries.length - 1; i >= 0; i--) { + const [id, p] = subEntries[i]; + if (id === draggedId) continue; + if (absoluteY >= p.top && absoluteY <= p.bottom) { + return id; + } } - } - for (const [id, p] of Object.entries(positions)) { - if (id === draggedId) continue; - if (absoluteY >= p.top && absoluteY <= p.bottom) { - return id; + for (const [id, p] of Object.entries(positions)) { + if (id === draggedId) continue; + if (absoluteY >= p.top && absoluteY <= p.bottom) { + return id; + } } - } - return null; - }, []); + return null; + }, + [] + ); - const calculateDropPosition = useCallback((absoluteY: number, targetId: string, positions: Record) => { - const target = positions[targetId]; - if (!target) return 'below' as const; - const middle = (target.top + target.bottom) / 2; - return absoluteY < middle ? 'above' : 'below'; - }, []); + const calculateDropPosition = useCallback( + ( + absoluteY: number, + targetId: string, + positions: Record + ) => { + const target = positions[targetId]; + if (!target) return 'below' as const; + const middle = (target.top + target.bottom) / 2; + return absoluteY < middle ? 'above' : 'below'; + }, + [] + ); - const handleDragStart = useCallback(async (taskId: string) => { - dragStateRef.current = { taskId, ...(await measureAll()) }; - }, [measureAll]); + const handleDragStart = useCallback( + async (taskId: string) => { + dragStateRef.current = { taskId, ...(await measureAll()) }; + }, + [measureAll] + ); const refreshDragPositions = useCallback(() => { - measureAll().then((m) => { + measureAll().then(m => { if (subtaskDragRef.current) { subtaskDragRef.current = { ...subtaskDragRef.current, ...m }; } else if (dragStateRef.current) { @@ -293,161 +370,217 @@ export function TaskList({ categoryIds = [], showCompleted = false, onSelectionC }); }, [measureAll]); - const updateHoverTarget = useCallback((target: string | null) => { - let changed = false; - setHoverTaskId((prev) => { - if (prev === target) return prev; - changed = true; - return target; - }); - if (changed && target) { - setTimeout(refreshDragPositions, 80); - } - }, [refreshDragPositions]); + const updateHoverTarget = useCallback( + (target: string | null) => { + let changed = false; + setHoverTaskId(prev => { + if (prev === target) return prev; + changed = true; + return target; + }); + if (changed && target) { + setTimeout(refreshDragPositions, 80); + } + }, + [refreshDragPositions] + ); - const handleDragUpdate = useCallback((absoluteY: number) => { - const state = dragStateRef.current; - if (!state) return; - const target = findHoverTarget(absoluteY, state.taskId, state.positions, state.subPositions); - updateHoverTarget(target); - if (target) { - if (state.subPositions[target]) { - setDropIndicator(null); + const handleDragUpdate = useCallback( + (absoluteY: number) => { + const state = dragStateRef.current; + if (!state) return; + const target = findHoverTarget(absoluteY, state.taskId, state.positions, state.subPositions); + updateHoverTarget(target); + if (target) { + if (state.subPositions[target]) { + setDropIndicator(null); + } else { + const position = calculateDropPosition(absoluteY, target, state.positions); + setDropIndicator({ targetId: target, position }); + } } else { - const position = calculateDropPosition(absoluteY, target, state.positions); - setDropIndicator({ targetId: target, position }); + // Check if below last item + const positions = state.positions; + const lastItem = Object.values(positions).reduce( + (max, p) => (p.bottom > max.bottom ? p : max), + { bottom: 0 } + ); + if (absoluteY > lastItem.bottom) { + setDropIndicator({ targetId: null, position: 'below' }); + } else { + setDropIndicator(null); + } } - } else { - // Check if below last item - const positions = state.positions; - const lastItem = Object.values(positions).reduce((max, p) => p.bottom > max.bottom ? p : max, { bottom: 0 }); - if (absoluteY > lastItem.bottom) { - setDropIndicator({ targetId: null, position: 'below' }); + }, + [findHoverTarget, calculateDropPosition, updateHoverTarget] + ); + + const handleDragEnd = useCallback( + (absoluteY: number) => { + const state = dragStateRef.current; + dragStateRef.current = null; + setHoverTaskId(null); + setDropIndicator(null); + if (!state) return; + + const target = findHoverTarget(absoluteY, state.taskId, state.positions, state.subPositions); + if (target) { + if (state.subPositions[target]) { + const parentTaskId = subtaskRefs.current.get(target)?.parentTaskId ?? state.taskId; + (async () => { + await convertTaskToSubtask(state.taskId, parentTaskId, target); + })(); + } else { + (async () => { + await convertTaskToSubtask(state.taskId, target); + })(); + } + } + }, + [findHoverTarget] + ); + + const handleSubtaskDragStart = useCallback( + async (subtaskId: string, parentTaskId: string, parentSubtaskId: string | null = null) => { + subtaskDragRef.current = { + subtaskId, + parentTaskId, + parentSubtaskId, + ...(await measureAll()), + }; + }, + [measureAll] + ); + + const handleSubtaskDragUpdate = useCallback( + (absoluteY: number) => { + const state = subtaskDragRef.current; + if (!state) return; + const target = findHoverTarget( + absoluteY, + state.subtaskId, + state.positions, + state.subPositions + ); + updateHoverTarget(target); + if (target) { + if (state.subPositions[target]) { + setDropIndicator(null); + } else { + const position = calculateDropPosition(absoluteY, target, state.positions); + setDropIndicator({ targetId: target, position }); + } } else { - setDropIndicator(null); + const positions = state.positions; + const lastItem = Object.values(positions).reduce( + (max, p) => (p.bottom > max.bottom ? p : max), + { bottom: 0 } + ); + if (absoluteY > lastItem.bottom) { + setDropIndicator({ targetId: null, position: 'below' }); + } else { + setDropIndicator(null); + } } - } - }, [findHoverTarget, calculateDropPosition, updateHoverTarget]); + }, + [findHoverTarget, calculateDropPosition, updateHoverTarget] + ); - const handleDragEnd = useCallback((absoluteY: number) => { - const state = dragStateRef.current; - dragStateRef.current = null; - setHoverTaskId(null); - setDropIndicator(null); - if (!state) return; + const handleSubtaskDragEnd = useCallback( + async (absoluteY: number) => { + const state = subtaskDragRef.current; + subtaskDragRef.current = null; + setHoverTaskId(null); + setDropIndicator(null); + if (!state) return; - const target = findHoverTarget(absoluteY, state.taskId, state.positions, state.subPositions); - if (target) { - if (state.subPositions[target]) { - const parentTaskId = subtaskRefs.current.get(target)?.parentTaskId ?? state.taskId; - (async () => { - await convertTaskToSubtask(state.taskId, parentTaskId, target); - })(); + const target = findHoverTarget( + absoluteY, + state.subtaskId, + state.positions, + state.subPositions + ); + if (target === state.subtaskId) return; + + // Only allow dropping on tasks (not subtasks) to match task behavior + // Check if target exists in main task positions, not subtask positions + const isTaskTarget = state.positions != null && target != null && target in state.positions; + if (target && isTaskTarget) { + // Move subtask to target task - makes it a subtask of that task + // If no target or target is a subtask: do nothing (matches task behavior) + await moveSubtaskToTask(state.subtaskId, target); + } + }, + [findHoverTarget, moveSubtaskToTask] + ); + + const handleTaskDragStart = useCallback( + async (taskId: string) => { + const positions = (await measureItems()) as Record; + taskDragRef.current = { taskId, positions }; + }, + [measureItems] + ); + + const handleTaskDragUpdate = useCallback( + (absoluteY: number) => { + const state = taskDragRef.current; + if (!state) return; + const target = findHoverTask(absoluteY, state.taskId, state.positions); + updateHoverTaskTarget(target); + if (target) { + setDropIndicator({ targetId: target, position: 'below' }); } else { - (async () => { - await convertTaskToSubtask(state.taskId, target); - })(); + const lastItem = Object.values(state.positions).reduce( + (max, p) => (p.bottom > max.bottom ? p : max), + { bottom: 0 } + ); + if (absoluteY > lastItem.bottom) { + setDropIndicator({ targetId: null, position: 'below' }); + } else { + setDropIndicator(null); + } } - } - }, [findHoverTarget]); + }, + [findHoverTask, updateHoverTaskTarget] + ); - const handleSubtaskDragStart = useCallback(async (subtaskId: string, parentTaskId: string, parentSubtaskId: string | null = null) => { - subtaskDragRef.current = { subtaskId, parentTaskId, parentSubtaskId, ...(await measureAll()) }; - }, [measureAll]); + const handleTaskDragEnd = useCallback( + async (absoluteY: number) => { + const state = taskDragRef.current; + taskDragRef.current = null; + setHoverTaskId(null); + setDropIndicator(null); + if (!state) return; - const handleSubtaskDragUpdate = useCallback((absoluteY: number) => { - const state = subtaskDragRef.current; - if (!state) return; - const target = findHoverTarget(absoluteY, state.subtaskId, state.positions, state.subPositions); - updateHoverTarget(target); - if (target) { - if (state.subPositions[target]) { - setDropIndicator(null); + const target = findHoverTask(absoluteY, state.taskId, state.positions); + if (target === state.taskId) return; + + // Set drop indicator based on target position + if (target) { + setDropIndicator({ targetId: target, position: 'below' }); } else { - const position = calculateDropPosition(absoluteY, target, state.positions); - setDropIndicator({ targetId: target, position }); + const lastItem = Object.values(state.positions).reduce( + (max, p) => (p.bottom > max.bottom ? p : max), + { bottom: 0 } + ); + if (absoluteY > lastItem.bottom) { + setDropIndicator({ targetId: null, position: 'below' }); + } } - } else { - const positions = state.positions; - const lastItem = Object.values(positions).reduce((max, p) => p.bottom > max.bottom ? p : max, { bottom: 0 }); - if (absoluteY > lastItem.bottom) { - setDropIndicator({ targetId: null, position: 'below' }); - } else { - setDropIndicator(null); - } - } - }, [findHoverTarget, calculateDropPosition, updateHoverTarget]); - -const handleSubtaskDragEnd = useCallback(async (absoluteY: number) => { - const state = subtaskDragRef.current; - subtaskDragRef.current = null; - setHoverTaskId(null); - setDropIndicator(null); - if (!state) return; - - const target = findHoverTarget(absoluteY, state.subtaskId, state.positions, state.subPositions); - if (target === state.subtaskId) return; - - // Only allow dropping on tasks (not subtasks) to match task behavior - // Check if target exists in main task positions, not subtask positions - const isTaskTarget = state.positions != null && target != null && target in state.positions; - if (target && isTaskTarget) { - // Move subtask to target task - makes it a subtask of that task - // If no target or target is a subtask: do nothing (matches task behavior) - await moveSubtaskToTask(state.subtaskId, target); - } - }, [findHoverTarget, moveSubtaskToTask]); - - const handleTaskDragStart = useCallback(async (taskId: string) => { - const positions = await measureItems() as Record; - taskDragRef.current = { taskId, positions }; - }, [measureItems]); - - const handleTaskDragUpdate = useCallback((absoluteY: number) => { - const state = taskDragRef.current; - if (!state) return; - const target = findHoverTask(absoluteY, state.taskId, state.positions); - updateHoverTaskTarget(target); - if (target) { - setDropIndicator({ targetId: target, position: 'below' }); - } else { - const lastItem = Object.values(state.positions).reduce((max, p) => p.bottom > max.bottom ? p : max, { bottom: 0 }); - if (absoluteY > lastItem.bottom) { - setDropIndicator({ targetId: null, position: 'below' }); - } else { - setDropIndicator(null); - } - } - }, [findHoverTask, updateHoverTaskTarget]); - - const handleTaskDragEnd = useCallback(async (absoluteY: number) => { - const state = taskDragRef.current; - taskDragRef.current = null; - setHoverTaskId(null); - setDropIndicator(null); - if (!state) return; - - const target = findHoverTask(absoluteY, state.taskId, state.positions); - if (target === state.taskId) return; - - // Set drop indicator based on target position - if (target) { - setDropIndicator({ targetId: target, position: 'below' }); - } else { - const lastItem = Object.values(state.positions).reduce((max, p) => p.bottom > max.bottom ? p : max, { bottom: 0 }); - if (absoluteY > lastItem.bottom) { - setDropIndicator({ targetId: null, position: 'below' }); - } - } - }, [findHoverTask]); + }, + [findHoverTask] + ); const renderItem = useCallback( ({ item, index }: { item: Task; index: number }) => { const isExpanded = expandedTasks.has(item.id); const itemSubtasks = subtasksMap.get(item.id) ?? []; - const showDropAbove = dropIndicator?.targetId === item.id && dropIndicator?.position === 'above'; - const showDropBelow = dropIndicator?.targetId === item.id && dropIndicator?.position === 'below'; - const tagColors = parseTaskTags(item.tags, item.categoryId).map((id) => categoryColors.get(id)); + const showDropAbove = + dropIndicator?.targetId === item.id && dropIndicator?.position === 'above'; + const showDropBelow = + dropIndicator?.targetId === item.id && dropIndicator?.position === 'below'; + const tagColors = parseTaskTags(item.tags, item.categoryId).map(id => categoryColors.get(id)); return ( {showDropAbove && } @@ -466,10 +599,10 @@ const handleSubtaskDragEnd = useCallback(async (absoluteY: number) => { onExpand={toggleExpand} onSelect={toggleSelect} onEnterSelection={enterSelection} - onMenuOpen={(task) => openTaskEdit(task.id)} + onMenuOpen={task => openTaskEdit(task.id)} onSubtaskToggle={handleSubtaskToggle} onSubtaskDelete={openSubtaskDelete} - onSubtaskMenuOpen={(subtask) => openSubtaskEdit(subtask.id)} + onSubtaskMenuOpen={subtask => openSubtaskEdit(subtask.id)} onDragStart={handleDragStart} onDragUpdate={handleDragUpdate} onDragEnd={handleDragEnd} @@ -523,7 +656,9 @@ const handleSubtaskDragEnd = useCallback(async (absoluteY: number) => { return ( No tasks yet - Tap + to add your first task + + Tap + to add your first task + ); }, [sortedTasks.length, theme.textSecondary, theme.textMuted]); @@ -545,9 +680,16 @@ const handleSubtaskDragEnd = useCallback(async (absoluteY: number) => { item.id} + keyExtractor={item => item.id} renderItem={renderItem} - extraData={{ expandedTasks, subtasksMap, selectedIds, selectionMode, hoverTaskId, dropIndicator }} + extraData={{ + expandedTasks, + subtasksMap, + selectedIds, + selectionMode, + hoverTaskId, + dropIndicator, + }} ItemSeparatorComponent={MemoSeparator} ListHeaderComponent={listHeader} ListFooterComponent={listFooter} @@ -566,16 +708,36 @@ const handleSubtaskDragEnd = useCallback(async (absoluteY: number) => { {modals(() => {})} {selectionMode && ( - - + + - + Cancel - {selectedIds.size} selected + + {selectedIds.size} selected + - + @@ -584,7 +746,15 @@ const handleSubtaskDragEnd = useCallback(async (absoluteY: number) => { ); } -const SelectionButton = React.memo(function SelectionButton({ label, color, onPress }: { label: string; color: string; onPress: () => void }) { +const SelectionButton = React.memo(function SelectionButton({ + label, + color, + onPress, +}: { + label: string; + color: string; + onPress: () => void; +}) { return ( registerRef(task.id, ref)} - style={styles.dragContainer} - > + registerRef(task.id, ref)} style={styles.dragContainer}> onToggle(task.id)} @@ -695,19 +862,19 @@ const TaskRow = React.memo(function TaskRow({ /> {expanded && subtasks.length > 0 && ( - {sortedSubtasks.map((sub) => ( - onSubtaskToggle(sub.id, task.id)} + {sortedSubtasks.map(sub => ( + onSubtaskToggle(sub.id, task.id)} onDelete={() => onSubtaskDelete(sub)} onMenuOpen={() => onSubtaskMenuOpen(sub)} selected={selectedIds.has(sub.id)} selectionMode={selectionMode} draggable - onDragStart={(sub) => onSubtaskDragStart(sub.id, task.id)} + onDragStart={sub => onSubtaskDragStart(sub.id, task.id)} onDragUpdate={onSubtaskDragUpdate} onDragEnd={onSubtaskDragEnd} categoryColor={categoryColor} @@ -716,8 +883,8 @@ const TaskRow = React.memo(function TaskRow({ ))} )} - - ); + + ); }); const styles = StyleSheet.create({ @@ -751,8 +918,7 @@ const styles = StyleSheet.create({ paddingTop: 4, marginBottom: 4, }, - dragContainer: { - }, + dragContainer: {}, dropIndicatorContainer: { height: 8, justifyContent: 'center', diff --git a/carry-your-live/src/components/TaskNameInput.tsx b/carry-your-live/src/components/TaskNameInput.tsx index 2404def..3332240 100644 --- a/carry-your-live/src/components/TaskNameInput.tsx +++ b/carry-your-live/src/components/TaskNameInput.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { View, Text, StyleSheet, TextInput , TextInputProps } from 'react-native'; +import { View, Text, StyleSheet, TextInput, TextInputProps } from 'react-native'; import { useSettings, type ThemeColors } from '@/theme'; interface TaskNameInputProps extends TextInputProps { @@ -35,37 +35,38 @@ export function TaskNameInput({ error, ...props }: TaskNameInputProps) { ); } -const createStyles = (theme: ThemeColors) => StyleSheet.create({ - container: { - gap: 5, - }, - labelRow: { - flexDirection: 'row', - alignItems: 'center', - }, - label: { - fontSize: 13, - fontWeight: '600', - }, - required: { - color: theme.dangerStrong, - fontSize: 13, - fontWeight: '600', - }, - input: { - height: 48, - paddingHorizontal: 14, - borderRadius: 10, - borderWidth: 1, - fontSize: 15, - }, - inputError: { - borderColor: theme.dangerStrong, - borderWidth: 1.5, - }, - errorText: { - fontSize: 11, - color: theme.dangerStrong, - marginLeft: 4, - }, -}); \ No newline at end of file +const createStyles = (theme: ThemeColors) => + StyleSheet.create({ + container: { + gap: 5, + }, + labelRow: { + flexDirection: 'row', + alignItems: 'center', + }, + label: { + fontSize: 13, + fontWeight: '600', + }, + required: { + color: theme.dangerStrong, + fontSize: 13, + fontWeight: '600', + }, + input: { + height: 48, + paddingHorizontal: 14, + borderRadius: 10, + borderWidth: 1, + fontSize: 15, + }, + inputError: { + borderColor: theme.dangerStrong, + borderWidth: 1.5, + }, + errorText: { + fontSize: 11, + color: theme.dangerStrong, + marginLeft: 4, + }, + }); diff --git a/carry-your-live/src/components/TaskOverflowMenu.tsx b/carry-your-live/src/components/TaskOverflowMenu.tsx index 3491579..d0dacc3 100644 --- a/carry-your-live/src/components/TaskOverflowMenu.tsx +++ b/carry-your-live/src/components/TaskOverflowMenu.tsx @@ -49,19 +49,45 @@ export function TaskOverflowMenu({ { key: 'edit', label: 'Edit', - icon: , + icon: ( + + + + ), onPress: onEdit, }, { key: 'complete', label: task.completed ? 'Mark Incomplete' : 'Mark Complete', - icon: , + icon: ( + + + + ), onPress: onToggleComplete, }, { key: 'duplicate', label: 'Duplicate', - icon: , + icon: ( + + + + ), onPress: onDuplicate, }, ]; @@ -70,7 +96,11 @@ export function TaskOverflowMenu({ actions.push({ key: 'category', label: 'Change Tags', - icon: , + icon: ( + + + + ), onPress: onChangeCategory ?? (() => {}), }); } @@ -79,23 +109,49 @@ export function TaskOverflowMenu({ { key: 'priority', label: 'Change Priority', - icon: , + icon: ( + + + + ), onPress: onChangePriority, }, { key: 'delete', label: 'Delete', destructive: true, - icon: , + icon: ( + + + + ), onPress: onDelete, - }, + } ); if (subtask && onAddSubtask) { actions.push({ key: 'addSubtask', label: 'Add Subtask', - icon: , + icon: ( + + + + ), onPress: onAddSubtask, }); } @@ -112,7 +168,7 @@ export function TaskOverflowMenu({ {task.title} - {actions.map((action) => ( + {actions.map(action => ( {action.icon} - + {action.label} @@ -140,54 +201,60 @@ function Circle2() { const { theme } = useSettings(); return ( - + ); } -const createStyles = (theme: ThemeColors) => StyleSheet.create({ - overlay: { - flex: 1, - justifyContent: 'flex-end', - }, - sheet: { - borderTopLeftRadius: 24, - borderTopRightRadius: 24, - paddingTop: 8, - paddingHorizontal: 16, - paddingBottom: 32, - }, - handle: { - width: 40, - height: 4, - borderRadius: 2, - alignSelf: 'center', - marginBottom: 12, - }, - title: { - fontSize: 14, - fontWeight: '600', - textAlign: 'center', - marginBottom: 16, - opacity: 0.8, - }, - action: { - flexDirection: 'row', - alignItems: 'center', - gap: 12, - paddingVertical: 14, - paddingHorizontal: 16, - borderRadius: 12, - borderWidth: 1, - marginBottom: 8, - }, - actionText: { - fontSize: 15, - fontWeight: '500', - }, - destructiveText: { - color: theme.dangerStrong, - fontWeight: '600', - }, -}); +const createStyles = (theme: ThemeColors) => + StyleSheet.create({ + overlay: { + flex: 1, + justifyContent: 'flex-end', + }, + sheet: { + borderTopLeftRadius: 24, + borderTopRightRadius: 24, + paddingTop: 8, + paddingHorizontal: 16, + paddingBottom: 32, + }, + handle: { + width: 40, + height: 4, + borderRadius: 2, + alignSelf: 'center', + marginBottom: 12, + }, + title: { + fontSize: 14, + fontWeight: '600', + textAlign: 'center', + marginBottom: 16, + opacity: 0.8, + }, + action: { + flexDirection: 'row', + alignItems: 'center', + gap: 12, + paddingVertical: 14, + paddingHorizontal: 16, + borderRadius: 12, + borderWidth: 1, + marginBottom: 8, + }, + actionText: { + fontSize: 15, + fontWeight: '500', + }, + destructiveText: { + color: theme.dangerStrong, + fontWeight: '600', + }, + }); diff --git a/carry-your-live/src/components/UpdateNotifier.tsx b/carry-your-live/src/components/UpdateNotifier.tsx index 4288887..f7ad672 100644 --- a/carry-your-live/src/components/UpdateNotifier.tsx +++ b/carry-your-live/src/components/UpdateNotifier.tsx @@ -1,14 +1,14 @@ import { useEffect, useState, useCallback } from 'react'; +import { Modal, View, Text, StyleSheet, Pressable, ScrollView, Platform } from 'react-native'; import { - Modal, - View, - Text, - StyleSheet, - Pressable, - ScrollView, - Platform, -} from 'react-native'; -import { checkForUpdates, wasPromptedFor, markPrompted, downloadAPK, installAPK, openReleaseUrl, AppUpdate } from '@/services/updates'; + checkForUpdates, + wasPromptedFor, + markPrompted, + downloadAPK, + installAPK, + openReleaseUrl, + AppUpdate, +} from '@/services/updates'; import { useSettings } from '@/theme'; export function UpdateNotifier() { @@ -79,12 +79,19 @@ export function UpdateNotifier() { const isMandatory = update.mandatory; return ( - {} : handleLater}> + {} : handleLater} + > Update Available - Version {update.version} + + Version {update.version} + @@ -127,7 +134,10 @@ export function UpdateNotifier() { {!isMandatory && ( - + Later )} @@ -142,11 +152,18 @@ export function UpdateNotifier() { disabled={downloading} > - {downloading ? 'Downloading...' : update.apkUrl ? 'Download & Install' : 'View Release'} + {downloading + ? 'Downloading...' + : update.apkUrl + ? 'Download & Install' + : 'View Release'} - + Release Notes @@ -275,4 +292,4 @@ const styles = StyleSheet.create({ fontSize: 14, fontWeight: '600', }, -}); \ No newline at end of file +}); diff --git a/carry-your-live/src/components/WheelTimePicker.tsx b/carry-your-live/src/components/WheelTimePicker.tsx index d6b86da..7c66396 100644 --- a/carry-your-live/src/components/WheelTimePicker.tsx +++ b/carry-your-live/src/components/WheelTimePicker.tsx @@ -53,7 +53,13 @@ interface WheelRowProps { onPress: () => void; } -const WheelRow = React.memo(function WheelRow({ label, selected, accent, textMuted, onPress }: WheelRowProps) { +const WheelRow = React.memo(function WheelRow({ + label, + selected, + accent, + textMuted, + onPress, +}: WheelRowProps) { return ( { - listRef.current?.scrollToOffset({ offset: initialIndex * ITEM_HEIGHT - (ITEM_HEIGHT * VISIBLE_ITEMS - ITEM_HEIGHT) / 2, animated: false }); + listRef.current?.scrollToOffset({ + offset: initialIndex * ITEM_HEIGHT - (ITEM_HEIGHT * VISIBLE_ITEMS - ITEM_HEIGHT) / 2, + animated: false, + }); indexRef.current = initialIndex; }, [initialIndex]); @@ -105,7 +114,10 @@ function WheelColumn({ const clamped = Math.min(Math.max(index, 0), data.length - 1); indexRef.current = clamped; onIndexChange(clamped); - listRef.current?.scrollToOffset({ offset: clamped * ITEM_HEIGHT - (ITEM_HEIGHT * VISIBLE_ITEMS - ITEM_HEIGHT) / 2, animated: true }); + listRef.current?.scrollToOffset({ + offset: clamped * ITEM_HEIGHT - (ITEM_HEIGHT * VISIBLE_ITEMS - ITEM_HEIGHT) / 2, + animated: true, + }); }, [data.length, onIndexChange] ); @@ -126,7 +138,9 @@ function WheelColumn({ const handleScroll = useCallback( (event: NativeSyntheticEvent) => { const offset = event.nativeEvent.contentOffset.y; - const index = Math.round((offset + (ITEM_HEIGHT * VISIBLE_ITEMS - ITEM_HEIGHT) / 2) / ITEM_HEIGHT); + const index = Math.round( + (offset + (ITEM_HEIGHT * VISIBLE_ITEMS - ITEM_HEIGHT) / 2) / ITEM_HEIGHT + ); const clamped = Math.min(Math.max(index, 0), data.length - 1); if (clamped !== indexRef.current) { indexRef.current = clamped; @@ -145,29 +159,32 @@ function WheelColumn({ }, []); return ( - String(item)} - renderItem={renderItem} - getItemLayout={(_data, index) => ({ - length: ITEM_HEIGHT, - offset: ITEM_HEIGHT * index, - index, - })} - initialScrollIndex={initialIndex} - snapToOffsets={data.map((_, i) => i * ITEM_HEIGHT)} - decelerationRate="normal" - showsVerticalScrollIndicator={false} - onScroll={handleScroll} - onScrollEndDrag={handleScrollEnd} - onMomentumScrollEnd={handleScrollEnd} - scrollEventThrottle={16} - style={[styles.column, { width }]} - contentContainerStyle={{ paddingBottom: (VISIBLE_ITEMS - 1) * ITEM_HEIGHT, alignItems: 'stretch' }} - snapToAlignment="center" - snapToInterval={ITEM_HEIGHT} - /> + String(item)} + renderItem={renderItem} + getItemLayout={(_data, index) => ({ + length: ITEM_HEIGHT, + offset: ITEM_HEIGHT * index, + index, + })} + initialScrollIndex={initialIndex} + snapToOffsets={data.map((_, i) => i * ITEM_HEIGHT)} + decelerationRate="normal" + showsVerticalScrollIndicator={false} + onScroll={handleScroll} + onScrollEndDrag={handleScrollEnd} + onMomentumScrollEnd={handleScrollEnd} + scrollEventThrottle={16} + style={[styles.column, { width }]} + contentContainerStyle={{ + paddingBottom: (VISIBLE_ITEMS - 1) * ITEM_HEIGHT, + alignItems: 'stretch', + }} + snapToAlignment="center" + snapToInterval={ITEM_HEIGHT} + /> ); } @@ -207,7 +224,10 @@ export function WheelTimePicker({ - + Cancel @@ -217,10 +237,16 @@ export function WheelTimePicker({ onConfirm(`${String(hourIndex).padStart(2, '0')}:${String(minuteIndex).padStart(2, '0')}`)} + onPress={() => + onConfirm( + `${String(hourIndex).padStart(2, '0')}:${String(minuteIndex).padStart(2, '0')}` + ) + } hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} > - Done + + Done + diff --git a/carry-your-live/src/constants/index.ts b/carry-your-live/src/constants/index.ts index 7a6cf6a..ed81809 100644 --- a/carry-your-live/src/constants/index.ts +++ b/carry-your-live/src/constants/index.ts @@ -43,4 +43,4 @@ export const PRIORITY_LABELS: Record = { medium: 'Medium', high: 'High', critical: 'Critical', -}; \ No newline at end of file +}; diff --git a/carry-your-live/src/database/adapter.native.ts b/carry-your-live/src/database/adapter.native.ts index e18196b..59d5a4b 100644 --- a/carry-your-live/src/database/adapter.native.ts +++ b/carry-your-live/src/database/adapter.native.ts @@ -6,7 +6,7 @@ export const adapter = new SQLiteAdapter({ schema, migrations, dbName: 'carryyourlive', - onSetUpError: (error) => { + onSetUpError: error => { console.error('Database setup error:', error); }, }); diff --git a/carry-your-live/src/database/adapter.ts b/carry-your-live/src/database/adapter.ts index 066f2de..e47936b 100644 --- a/carry-your-live/src/database/adapter.ts +++ b/carry-your-live/src/database/adapter.ts @@ -1,2 +1,2 @@ import { adapter } from './adapter.native'; -export { adapter }; \ No newline at end of file +export { adapter }; diff --git a/carry-your-live/src/database/adapter.web.ts b/carry-your-live/src/database/adapter.web.ts index b6bdcd9..2981906 100644 --- a/carry-your-live/src/database/adapter.web.ts +++ b/carry-your-live/src/database/adapter.web.ts @@ -8,7 +8,7 @@ export const adapter = new LokiJSAdapter({ dbName: 'carryyourlive', useWebWorker: false, useIncrementalIndexedDB: true, - onSetUpError: (error) => { + onSetUpError: error => { console.error('Database setup error:', error); }, }); diff --git a/carry-your-live/src/database/migrations.ts b/carry-your-live/src/database/migrations.ts index 6591df3..6d007eb 100644 --- a/carry-your-live/src/database/migrations.ts +++ b/carry-your-live/src/database/migrations.ts @@ -1,4 +1,9 @@ -import { schemaMigrations, addColumns, createTable, unsafeExecuteSql } from '@nozbe/watermelondb/Schema/migrations'; +import { + schemaMigrations, + addColumns, + createTable, + unsafeExecuteSql, +} from '@nozbe/watermelondb/Schema/migrations'; export const migrations = schemaMigrations({ migrations: [ @@ -152,11 +157,21 @@ export const migrations = schemaMigrations({ unsafeExecuteSql('CREATE INDEX IF NOT EXISTS idx_tasks_updated_at ON tasks(updated_at);'), unsafeExecuteSql('CREATE INDEX IF NOT EXISTS idx_tasks_created_at ON tasks(created_at);'), unsafeExecuteSql('CREATE INDEX IF NOT EXISTS idx_tasks_completed ON tasks(completed);'), - unsafeExecuteSql('CREATE INDEX IF NOT EXISTS idx_subtasks_updated_at ON subtasks(updated_at);'), - unsafeExecuteSql('CREATE INDEX IF NOT EXISTS idx_subtasks_created_at ON subtasks(created_at);'), - unsafeExecuteSql('CREATE INDEX IF NOT EXISTS idx_categories_updated_at ON categories(updated_at);'), - unsafeExecuteSql('CREATE INDEX IF NOT EXISTS idx_repeat_profiles_updated_at ON repeat_profiles(updated_at);'), - unsafeExecuteSql('CREATE INDEX IF NOT EXISTS idx_friendships_updated_at ON friendships(updated_at);'), + unsafeExecuteSql( + 'CREATE INDEX IF NOT EXISTS idx_subtasks_updated_at ON subtasks(updated_at);' + ), + unsafeExecuteSql( + 'CREATE INDEX IF NOT EXISTS idx_subtasks_created_at ON subtasks(created_at);' + ), + unsafeExecuteSql( + 'CREATE INDEX IF NOT EXISTS idx_categories_updated_at ON categories(updated_at);' + ), + unsafeExecuteSql( + 'CREATE INDEX IF NOT EXISTS idx_repeat_profiles_updated_at ON repeat_profiles(updated_at);' + ), + unsafeExecuteSql( + 'CREATE INDEX IF NOT EXISTS idx_friendships_updated_at ON friendships(updated_at);' + ), unsafeExecuteSql('CREATE INDEX IF NOT EXISTS idx_tasks_series_id ON tasks(series_id);'), ], }, diff --git a/carry-your-live/src/database/schema.ts b/carry-your-live/src/database/schema.ts index 8278a26..7dff855 100644 --- a/carry-your-live/src/database/schema.ts +++ b/carry-your-live/src/database/schema.ts @@ -97,4 +97,4 @@ export const schema = appSchema({ ], }), ], -}); \ No newline at end of file +}); diff --git a/carry-your-live/src/database/sync.ts b/carry-your-live/src/database/sync.ts index a4ef6f9..48f891b 100644 --- a/carry-your-live/src/database/sync.ts +++ b/carry-your-live/src/database/sync.ts @@ -17,8 +17,20 @@ const LAST_PUSHED_AT_KEY = 'sync:lastPushedAt'; const LAST_RUN_AT_KEY = 'sync:lastRunAt'; export interface SyncResult { - pushed: { categories: number; tasks: number; subtasks: number; repeatProfiles: number; friendships: number }; - pulled: { categories: number; tasks: number; subtasks: number; repeatProfiles: number; friendships: number }; + pushed: { + categories: number; + tasks: number; + subtasks: number; + repeatProfiles: number; + friendships: number; + }; + pulled: { + categories: number; + tasks: number; + subtasks: number; + repeatProfiles: number; + friendships: number; + }; conflicts: number; timestamp: number; } @@ -150,7 +162,10 @@ export async function runSync(): Promise { return result; } -async function pushChanges(): Promise<{ conflicts: PushConflict[]; pushedDeletions: { entity: TombstoneEntity; id: string }[] }> { +async function pushChanges(): Promise<{ + conflicts: PushConflict[]; + pushedDeletions: { entity: TombstoneEntity; id: string }[]; +}> { const lastPushedAt = await getLastPushedAt(); const [tasks, subtasks, repeatProfiles, friendships] = await Promise.all([ @@ -195,7 +210,7 @@ async function pushChanges(): Promise<{ conflicts: PushConflict[]; pushedDeletio if (t.categoryId) referencedCategoryIds.add(t.categoryId); } - const changedSubtasks = subtasks.map((s) => ({ + const changedSubtasks = subtasks.map(s => ({ id: s.id, taskId: s.taskId, parentSubtaskId: s.parentSubtaskId || null, @@ -251,7 +266,7 @@ async function pushChanges(): Promise<{ conflicts: PushConflict[]; pushedDeletio } const categories = await categoryQuery.fetch(); - const changedCategories = categories.map((c) => ({ + const changedCategories = categories.map(c => ({ id: c.id, name: c.name, color: c.color, @@ -260,7 +275,7 @@ async function pushChanges(): Promise<{ conflicts: PushConflict[]; pushedDeletio updatedAt: c.updatedAt.getTime(), })); - const changedRepeatProfiles = repeatProfiles.map((p) => ({ + const changedRepeatProfiles = repeatProfiles.map(p => ({ id: p.id, name: p.name, repeat: p.repeat, @@ -270,7 +285,7 @@ async function pushChanges(): Promise<{ conflicts: PushConflict[]; pushedDeletio updatedAt: p.updatedAt.getTime(), })); - const changedFriendships = friendships.map((f) => ({ + const changedFriendships = friendships.map(f => ({ id: f.id, userId: f.userId, friendId: f.friendId, @@ -280,7 +295,7 @@ async function pushChanges(): Promise<{ conflicts: PushConflict[]; pushedDeletio })); const pendingTombstones = await fetchPendingTombstones(lastPushedAt); - const changedDeletions = pendingTombstones.map((t) => ({ + const changedDeletions = pendingTombstones.map(t => ({ entity: t.entity, id: t.entityId, updatedAt: t.deletedAt.getTime(), @@ -315,7 +330,7 @@ async function pushChanges(): Promise<{ conflicts: PushConflict[]; pushedDeletio const data = await response.json(); return { conflicts: (data.conflicts ?? []) as PushConflict[], - pushedDeletions: pendingTombstones.map((t) => ({ entity: t.entity, id: t.entityId })), + pushedDeletions: pendingTombstones.map(t => ({ entity: t.entity, id: t.entityId })), }; } @@ -327,8 +342,8 @@ async function pruneSyncedTombstones( pushedDeletions: { entity: TombstoneEntity; id: string }[] ): Promise { if (pushedDeletions.length === 0) return; - const conflictKeys = new Set(conflicts.map((c) => `${c.entity}:${c.id}`)); - const accepted = pushedDeletions.filter((d) => !conflictKeys.has(`${d.entity}:${d.id}`)); + const conflictKeys = new Set(conflicts.map(c => `${c.entity}:${c.id}`)); + const accepted = pushedDeletions.filter(d => !conflictKeys.has(`${d.entity}:${d.id}`)); const grouped = new Map(); for (const t of accepted) { const list = grouped.get(t.entity) ?? []; @@ -412,7 +427,11 @@ async function applyServerChanges(data: { // Apply a server-side deletion to the local DB, cascading to every related // row (category -> tasks -> subtasks, task -> subtasks, subtask -> children). // LWW: if the local row is newer than the deletion, it survives. -async function applyRemoteTombstone(tomb: { entity: TombstoneEntity; id: string; updatedAt: number }): Promise { +async function applyRemoteTombstone(tomb: { + entity: TombstoneEntity; + id: string; + updatedAt: number; +}): Promise { const { entity, id, updatedAt } = tomb; const row = await findOrNull(collections[entity as keyof typeof collections] as any, id); if (row) { @@ -474,7 +493,7 @@ async function destroyLocalEntityCascade(entity: TombstoneEntity, id: string): P async function upsertCategory(row: ServerRow, force: boolean): Promise { const local = await findOrNull(collections.categories, row.id); if (!local) { - await collections.categories.create((c) => { + await collections.categories.create(c => { c.name = String(row.name ?? ''); c.color = String(row.color ?? '#9E9E9E'); c.order = Number(row.order ?? 0); @@ -486,7 +505,7 @@ async function upsertCategory(row: ServerRow, force: boolean): Promise if (!force && local.updatedAt.getTime() > Number(row.updatedAt ?? 0)) { return false; } - await local.update((c) => { + await local.update(c => { c.name = String(row.name ?? c.name); c.color = String(row.color ?? c.color); c.order = Number(row.order ?? c.order); @@ -498,7 +517,7 @@ async function upsertCategory(row: ServerRow, force: boolean): Promise async function upsertTask(row: ServerRow, force: boolean): Promise { const local = await findOrNull(collections.tasks, row.id); if (!local) { - await collections.tasks.create((t) => { + await collections.tasks.create(t => { t.title = String(row.title ?? ''); t.description = String(row.description ?? ''); t.categoryId = String(row.categoryId ?? ''); @@ -524,7 +543,7 @@ async function upsertTask(row: ServerRow, force: boolean): Promise { if (!force && local.updatedAt.getTime() > Number(row.updatedAt ?? 0)) { return false; } - await local.update((t) => { + await local.update(t => { t.title = String(row.title ?? t.title); t.description = String(row.description ?? t.description); t.categoryId = String(row.categoryId ?? t.categoryId); @@ -550,7 +569,7 @@ async function upsertTask(row: ServerRow, force: boolean): Promise { async function upsertRepeatProfile(row: ServerRow, force: boolean): Promise { const local = await findOrNull(collections.repeatProfiles, row.id); if (!local) { - await collections.repeatProfiles.create((p) => { + await collections.repeatProfiles.create(p => { p.name = String(row.name ?? ''); p.repeat = row.repeat ?? 'none'; p.repeatInterval = Number(row.repeatInterval ?? 1); @@ -563,7 +582,7 @@ async function upsertRepeatProfile(row: ServerRow, force: boolean): Promise Number(row.updatedAt ?? 0)) { return false; } - await local.update((p) => { + await local.update(p => { p.name = String(row.name ?? p.name); p.repeat = row.repeat ?? p.repeat; p.repeatInterval = Number(row.repeatInterval ?? p.repeatInterval); @@ -579,7 +598,7 @@ async function upsertSubtask(row: ServerRow, force: boolean): Promise { const local = await findOrNull(collections.subtasks, row.id); if (!local) { - await collections.subtasks.create((s) => { + await collections.subtasks.create(s => { s.taskId = String(row.taskId ?? ''); s.parentSubtaskId = row.parentSubtaskId == null ? null : String(row.parentSubtaskId); s.categoryId = row.categoryId ? String(row.categoryId) : ''; @@ -607,7 +626,7 @@ async function upsertSubtask(row: ServerRow, force: boolean): Promise { if (!force && local.updatedAt.getTime() > Number(row.updatedAt ?? 0)) { return false; } - await local.update((s) => { + await local.update(s => { s.taskId = String(row.taskId ?? s.taskId); s.parentSubtaskId = row.parentSubtaskId == null ? null : String(row.parentSubtaskId); s.categoryId = row.categoryId ? String(row.categoryId) : s.categoryId; @@ -635,7 +654,7 @@ async function upsertSubtask(row: ServerRow, force: boolean): Promise { async function upsertFriendship(row: ServerRow, force: boolean): Promise { const local = await findOrNull(collections.friendships, row.id); if (!local) { - await collections.friendships.create((f) => { + await collections.friendships.create(f => { f.userId = String(row.userId ?? ''); f.friendId = String(row.friendId ?? ''); f.status = (row.status as 'pending' | 'accepted') ?? 'pending'; @@ -647,7 +666,7 @@ async function upsertFriendship(row: ServerRow, force: boolean): Promise Number(row.updatedAt ?? 0)) { return false; } - await local.update((f) => { + await local.update(f => { f.userId = String(row.userId ?? f.userId); f.friendId = String(row.friendId ?? f.friendId); f.status = (row.status as 'pending' | 'accepted') ?? f.status; @@ -749,4 +768,4 @@ export function isSyncInFlight(): boolean { export function isWatchingForUpdates(): boolean { return watcherActive; -} \ No newline at end of file +} diff --git a/carry-your-live/src/database/tombstones.ts b/carry-your-live/src/database/tombstones.ts index 1381078..2eb1744 100644 --- a/carry-your-live/src/database/tombstones.ts +++ b/carry-your-live/src/database/tombstones.ts @@ -3,7 +3,10 @@ import { Q } from '@nozbe/watermelondb'; import Tombstone, { TombstoneEntity } from '@/models/Tombstone'; // Batch variant: must be called from inside an existing database.write(). -export async function recordTombstonesInBatch(entity: TombstoneEntity, ids: string[]): Promise { +export async function recordTombstonesInBatch( + entity: TombstoneEntity, + ids: string[] +): Promise { const uniqueIds = [...new Set(ids)].filter(Boolean); for (const id of uniqueIds) { const existing = await collections.tombstones @@ -11,11 +14,11 @@ export async function recordTombstonesInBatch(entity: TombstoneEntity, ids: stri .fetch(); const now = new Date(); if (existing.length > 0) { - await existing[0].update((t) => { + await existing[0].update(t => { t.deletedAt = now; }); } else { - await collections.tombstones.create((t) => { + await collections.tombstones.create(t => { t.entity = entity; t.entityId = id; t.deletedAt = now; @@ -25,7 +28,10 @@ export async function recordTombstonesInBatch(entity: TombstoneEntity, ids: stri } // Batch variant: must be called from inside an existing database.write(). -export async function removeTombstonesInBatch(entity: TombstoneEntity, ids: string[]): Promise { +export async function removeTombstonesInBatch( + entity: TombstoneEntity, + ids: string[] +): Promise { for (const id of ids) { const existing = await collections.tombstones .query(Q.where('entity', entity), Q.where('entity_id', id)) diff --git a/carry-your-live/src/hooks/useDatabase.tsx b/carry-your-live/src/hooks/useDatabase.tsx index 1dd7973..229729f 100644 --- a/carry-your-live/src/hooks/useDatabase.tsx +++ b/carry-your-live/src/hooks/useDatabase.tsx @@ -23,11 +23,11 @@ export function DatabaseProvider({ children }: { children: ReactNode }) { const initializeDatabase = async () => { try { const existingCategories = await collections.categories.query().fetch(); - + // Merge categories with the same spelling (case-insensitive) - const categoriesByName = new Map(); + const categoriesByName = new Map(); const duplicates: typeof existingCategories = []; - + for (const cat of existingCategories) { const key = cat.name.toLowerCase(); if (categoriesByName.has(key)) { @@ -36,28 +36,32 @@ export function DatabaseProvider({ children }: { children: ReactNode }) { categoriesByName.set(key, cat); } } - + if (duplicates.length > 0) { await database.write(async () => { for (const dup of duplicates) { const keep = categoriesByName.get(dup.name.toLowerCase())!; // Reassign tasks from duplicate to kept category - const tasksToUpdate = await collections.tasks.query(Q.where('category_id', dup.id)).fetch(); + const tasksToUpdate = await collections.tasks + .query(Q.where('category_id', dup.id)) + .fetch(); for (const task of tasksToUpdate) { - await task.update((t) => { t.categoryId = keep.id; }); + await task.update(t => { + t.categoryId = keep.id; + }); } // Delete duplicate category await dup.destroyPermanently(); } }); } - + const freshCategories = await collections.categories.query().fetch(); - + if (freshCategories.length === 0) { await database.write(async () => { for (const cat of DEFAULT_CATEGORIES) { - await collections.categories.create((c) => { + await collections.categories.create(c => { c.name = cat.name; c.color = cat.color; c.order = cat.order; @@ -67,7 +71,7 @@ export function DatabaseProvider({ children }: { children: ReactNode }) { } }); } - + setIsReady(true); const token = await getAuthToken(); @@ -115,9 +119,9 @@ export function useCategories(): Category[] { const subscription = collections.categories .query() .observeWithColumns(['name', 'color', 'order']) - .pipe(map((cats) => [...cats].sort((a, b) => a.order - b.order))) + .pipe(map(cats => [...cats].sort((a, b) => a.order - b.order))) .subscribe({ - next: (cats) => { + next: cats => { if (mounted) setCategories(cats); }, error: () => {}, @@ -154,9 +158,9 @@ export function useRepeatProfiles(): RepeatProfile[] { const subscription = collections.repeatProfiles .query() .observeWithColumns(['name', 'repeat', 'repeat_interval', 'repeat_days']) - .pipe(map((rows) => [...rows].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()))) + .pipe(map(rows => [...rows].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()))) .subscribe({ - next: (rows) => { + next: rows => { if (mounted) setProfiles(rows); }, error: () => {}, @@ -169,4 +173,4 @@ export function useRepeatProfiles(): RepeatProfile[] { }, [collections]); return profiles; -} \ No newline at end of file +} diff --git a/carry-your-live/src/hooks/useFriends.tsx b/carry-your-live/src/hooks/useFriends.tsx index aa37c1c..a9f4faa 100644 --- a/carry-your-live/src/hooks/useFriends.tsx +++ b/carry-your-live/src/hooks/useFriends.tsx @@ -1,5 +1,14 @@ import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; -import { getFriends, sendFriendRequest, acceptFriendRequest, deleteFriendRequest, removeFriend, searchUsers, Friend, FriendRequest } from '@/services/friends'; +import { + getFriends, + sendFriendRequest, + acceptFriendRequest, + deleteFriendRequest, + removeFriend, + searchUsers, + Friend, + FriendRequest, +} from '@/services/friends'; interface FriendsContextType { friends: Friend[]; @@ -95,4 +104,4 @@ export function useFriends(): FriendsContextType { throw new Error('useFriends must be used within a FriendsProvider'); } return context; -} \ No newline at end of file +} diff --git a/carry-your-live/src/hooks/useICalImport.ts b/carry-your-live/src/hooks/useICalImport.ts index 292fbba..dadf061 100644 --- a/carry-your-live/src/hooks/useICalImport.ts +++ b/carry-your-live/src/hooks/useICalImport.ts @@ -43,7 +43,11 @@ function mapICalEventToTask(event: { let allDay = event.allDay === true || !event.end; if (event.allDay) { - dueDate = new Date(event.start.getFullYear(), event.start.getMonth(), event.start.getDate()).getTime(); + dueDate = new Date( + event.start.getFullYear(), + event.start.getMonth(), + event.start.getDate() + ).getTime(); dueTime = '00:00'; endTime = '23:59'; } else { @@ -143,8 +147,8 @@ export function useICalImport() { // Insert tasks into WatermelonDB await Promise.all( - taskData.map((td) => - collections.tasks.create((t) => { + taskData.map(td => + collections.tasks.create(t => { t.title = td.title; t.description = td.description; t.categoryId = td.categoryId; @@ -177,4 +181,4 @@ export function useICalImport() { }, []); return { importICal, loading, error }; -} \ No newline at end of file +} diff --git a/carry-your-live/src/hooks/useOnline.ts b/carry-your-live/src/hooks/useOnline.ts index a0fd5dd..9cd3c92 100644 --- a/carry-your-live/src/hooks/useOnline.ts +++ b/carry-your-live/src/hooks/useOnline.ts @@ -14,4 +14,4 @@ export function useOnline(): boolean { }, []); return isOnline; -} \ No newline at end of file +} diff --git a/carry-your-live/src/hooks/useStats.tsx b/carry-your-live/src/hooks/useStats.tsx index 07c8035..67b57a7 100644 --- a/carry-your-live/src/hooks/useStats.tsx +++ b/carry-your-live/src/hooks/useStats.tsx @@ -83,10 +83,7 @@ export function useStats(): TaskStats { let mounted = true; const compute = () => { - Promise.all([ - collections.tasks.query().fetch(), - collections.categories.query().fetch(), - ]) + Promise.all([collections.tasks.query().fetch(), collections.categories.query().fetch()]) .then(([allTasks, categories]) => { const now = Date.now(); const todayStart = startOfDay(now); @@ -129,25 +126,25 @@ export function useStats(): TaskStats { }); } - const categoryMap = new Map(categories.map((c) => [c.id, c])); + const categoryMap = new Map(categories.map(c => [c.id, c])); const byCategory: CategoryStat[] = Array.from(categoryMap.values()) - .map((c) => ({ + .map(c => ({ id: c.id, name: c.name, color: c.color, count: categoryCounts.get(c.id) ?? 0, })) - .filter((c) => c.count > 0) + .filter(c => c.count > 0) .sort((a, b) => b.count - a.count); const priorityOrder = ['high', 'medium', 'low', 'none']; const byPriority: PriorityStat[] = priorityOrder - .map((p) => ({ + .map(p => ({ priority: p, label: PRIORITY_LABELS[p], count: priorityCounts.get(p) ?? 0, })) - .filter((p) => p.count > 0); + .filter(p => p.count > 0); let streak = 0; const todayCount = dayBucket.get(todayStart) ?? 0; @@ -165,9 +162,10 @@ export function useStats(): TaskStats { completedLast30, activeCount, overdueCount, - completionRate: totalCompleted > 0 - ? Math.round((totalCompleted / (totalCompleted + activeCount)) * 100) - : 0, + completionRate: + totalCompleted > 0 + ? Math.round((totalCompleted / (totalCompleted + activeCount)) * 100) + : 0, daily, byCategory, byPriority, @@ -176,18 +174,21 @@ export function useStats(): TaskStats { } }) .catch(() => { - if (mounted) setStats((s) => ({ ...s, loading: false })); + if (mounted) setStats(s => ({ ...s, loading: false })); }); }; compute(); - const subscription = collections.tasks.query().observe().subscribe({ - next: () => { - compute(); - }, - error: () => {}, - }); + const subscription = collections.tasks + .query() + .observe() + .subscribe({ + next: () => { + compute(); + }, + error: () => {}, + }); return () => { mounted = false; @@ -196,4 +197,4 @@ export function useStats(): TaskStats { }, [collections, isReady]); return stats; -} \ No newline at end of file +} diff --git a/carry-your-live/src/hooks/useSubtasks.ts b/carry-your-live/src/hooks/useSubtasks.ts index e8dd00a..634b4da 100644 --- a/carry-your-live/src/hooks/useSubtasks.ts +++ b/carry-your-live/src/hooks/useSubtasks.ts @@ -62,7 +62,7 @@ export function useSubtasks(): { map: Map; refresh: () => const { collections } = useDatabase(); const [rows, setRows] = useState([]); const [refreshKey, setRefreshKey] = useState(0); - const refresh = useCallback(() => setRefreshKey((k) => k + 1), []); + const refresh = useCallback(() => setRefreshKey(k => k + 1), []); useEffect(() => { let mounted = true; @@ -70,7 +70,7 @@ export function useSubtasks(): { map: Map; refresh: () => .query() .observe() .subscribe({ - next: (result) => { + next: result => { if (mounted) setRows(result); }, error: () => {}, @@ -84,4 +84,4 @@ export function useSubtasks(): { map: Map; refresh: () => const map = useMemo(() => buildTrees(rows), [rows]); return { map, refresh }; -} \ No newline at end of file +} diff --git a/carry-your-live/src/hooks/useTaskModals.tsx b/carry-your-live/src/hooks/useTaskModals.tsx index a2064b8..859205c 100644 --- a/carry-your-live/src/hooks/useTaskModals.tsx +++ b/carry-your-live/src/hooks/useTaskModals.tsx @@ -22,7 +22,10 @@ import { confirmAndToggleTaskComplete, } from '@/utils/taskActions'; -const PRIORITY_OPTIONS = Object.entries(PRIORITY_LABELS).map(([value, label]) => ({ value, label })); +const PRIORITY_OPTIONS = Object.entries(PRIORITY_LABELS).map(([value, label]) => ({ + value, + label, +})); interface AddSubtaskModalProps { visible: boolean; @@ -122,9 +125,7 @@ function AddSubtaskModal({ visible, parentSubtask, onClose, onAdd }: AddSubtaskM > - - Add Subtask to “{parentSubtask.title}” - + Add Subtask to “{parentSubtask.title}” (null); const [deleteTarget, setDeleteTarget] = useState(null); const [deleteSubtaskTarget, setDeleteSubtaskTarget] = useState(null); - const [picker, setPicker] = useState(null); - const [subtaskPicker, setSubtaskPicker] = useState(null); + const [picker, setPicker] = useState( + null + ); + const [subtaskPicker, setSubtaskPicker] = useState(null); const [addSubtaskTarget, setAddSubtaskTarget] = useState(null); const refreshRef = useRef<() => void>(() => {}); const openTaskMenu = useCallback((task: TaskData) => setMenuTask(task), []); const openSubtaskMenu = useCallback((subtask: SubtaskData) => setMenuSubtask(subtask), []); const openTaskDelete = useCallback((task: TaskData) => setDeleteTarget(task), []); - const openSubtaskDelete = useCallback((subtask: SubtaskData) => setDeleteSubtaskTarget(subtask), []); - const openTaskEdit = useCallback((taskId: string) => { - router.push({ pathname: '/task-detail', params: { id: taskId } }); - }, [router]); - const openSubtaskEdit = useCallback((subtaskId: string) => { - router.push({ pathname: '/subtask-detail', params: { id: subtaskId } }); - }, [router]); + const openSubtaskDelete = useCallback( + (subtask: SubtaskData) => setDeleteSubtaskTarget(subtask), + [] + ); + const openTaskEdit = useCallback( + (taskId: string) => { + router.push({ pathname: '/task-detail', params: { id: taskId } }); + }, + [router] + ); + const openSubtaskEdit = useCallback( + (subtaskId: string) => { + router.push({ pathname: '/subtask-detail', params: { id: subtaskId } }); + }, + [router] + ); const openSubtaskAdd = useCallback((subtask: SubtaskData) => { setAddSubtaskTarget(subtask); }, []); @@ -197,13 +212,16 @@ export function useTaskModals() { refreshRef.current(); }, []); - const handleDeleteScope = useCallback(async (scope: TaskDeleteScope) => { - const target = deleteTarget; - if (!target) return; - setDeleteTarget(null); - await deleteTaskOccurrences(target.id, scope); - refreshRef.current(); - }, [deleteTarget]); + const handleDeleteScope = useCallback( + async (scope: TaskDeleteScope) => { + const target = deleteTarget; + if (!target) return; + setDeleteTarget(null); + await deleteTaskOccurrences(target.id, scope); + refreshRef.current(); + }, + [deleteTarget] + ); const handleDeleteSubtask = useCallback(async () => { const target = deleteSubtaskTarget; @@ -213,38 +231,56 @@ export function useTaskModals() { refreshRef.current(); }, [deleteSubtaskTarget]); - const handleSingleCategory = useCallback(async (value: string | string[]) => { - if (picker?.taskId) { - await setTaskCategory(picker.taskId, Array.isArray(value) ? value[0] : value); - refreshRef.current(); - } - }, [picker]); + const handleSingleCategory = useCallback( + async (value: string | string[]) => { + if (picker?.taskId) { + await setTaskCategory(picker.taskId, Array.isArray(value) ? value[0] : value); + refreshRef.current(); + } + }, + [picker] + ); - const handleSinglePriority = useCallback(async (value: string | string[]) => { - if (picker?.taskId) { - await setTaskPriority(picker.taskId, (Array.isArray(value) ? value[0] : value) as TaskData['priority']); - refreshRef.current(); - } - }, [picker]); + const handleSinglePriority = useCallback( + async (value: string | string[]) => { + if (picker?.taskId) { + await setTaskPriority( + picker.taskId, + (Array.isArray(value) ? value[0] : value) as TaskData['priority'] + ); + refreshRef.current(); + } + }, + [picker] + ); - const handleSubtaskPriority = useCallback(async (value: string | string[]) => { - if (subtaskPicker?.subtaskId) { - await setSubtaskPriority(subtaskPicker.subtaskId, (Array.isArray(value) ? value[0] : value) as TaskData['priority']); - refreshRef.current(); - } - }, [subtaskPicker]); + const handleSubtaskPriority = useCallback( + async (value: string | string[]) => { + if (subtaskPicker?.subtaskId) { + await setSubtaskPriority( + subtaskPicker.subtaskId, + (Array.isArray(value) ? value[0] : value) as TaskData['priority'] + ); + refreshRef.current(); + } + }, + [subtaskPicker] + ); - const handleAddSubtask = useCallback(async (title: string) => { - const target = addSubtaskTarget; - if (!target || !title.trim()) return; - await createSubtask({ - taskId: target.taskId, - parentSubtaskId: target.id, - title: title.trim(), - }); - setAddSubtaskTarget(null); - refreshRef.current(); - }, [addSubtaskTarget]); + const handleAddSubtask = useCallback( + async (title: string) => { + const target = addSubtaskTarget; + if (!target || !title.trim()) return; + await createSubtask({ + taskId: target.taskId, + parentSubtaskId: target.id, + title: title.trim(), + }); + setAddSubtaskTarget(null); + refreshRef.current(); + }, + [addSubtaskTarget] + ); const modals = (refresh: () => void) => { refreshRef.current = refresh; @@ -289,7 +325,9 @@ export function useTaskModals() { onDelete={() => menuSubtask && openSubtaskDelete(menuSubtask)} onDuplicate={() => menuSubtask && handleSubtaskDuplicate(menuSubtask.id)} onToggleComplete={() => menuSubtask && handleSubtaskToggle(menuSubtask)} - onChangePriority={() => menuSubtask && setSubtaskPicker({ type: 'priority', subtaskId: menuSubtask.id })} + onChangePriority={() => + menuSubtask && setSubtaskPicker({ type: 'priority', subtaskId: menuSubtask.id }) + } onAddSubtask={() => menuSubtask && openSubtaskAdd(menuSubtask)} /> @@ -303,7 +341,10 @@ export function useTaskModals() { ({ value: c.id, label: c.name, color: c.color }))]} + options={[ + { value: '', label: 'None', color: '#9E9E9E' }, + ...categories.map(c => ({ value: c.id, label: c.name, color: c.color })), + ]} selectedValue={menuTask?.categoryId} onSelect={handleSingleCategory} onClose={() => setPicker(null)} @@ -318,7 +359,7 @@ export function useTaskModals() { onClose={() => setPicker(null)} /> -([]); const [loading, setLoading] = useState(true); const [refreshKey, setRefreshKey] = useState(0); - const refresh = useCallback(() => setRefreshKey((k) => k + 1), []); + const refresh = useCallback(() => setRefreshKey(k => k + 1), []); const range = useMemo(() => { const s = startOfMonth(monthDate); @@ -23,13 +23,10 @@ export function useTasksInMonth(monthDate: Date) { let mounted = true; setLoading(true); const subscription = collections.tasks - .query( - Q.where('due_date', Q.between(range.start, range.end)), - Q.sortBy('due_date', 'asc') - ) + .query(Q.where('due_date', Q.between(range.start, range.end)), Q.sortBy('due_date', 'asc')) .observe() .subscribe({ - next: (result) => { + next: result => { if (mounted) { setTasks(result); setLoading(false); @@ -60,12 +57,16 @@ export function useTasksInMonth(monthDate: Date) { return { tasks, byDay, loading, refresh }; } -export function useTasks(categoryIds: string[] = [], showCompleted: boolean | 'all' = false, maxAheadDays?: number) { +export function useTasks( + categoryIds: string[] = [], + showCompleted: boolean | 'all' = false, + maxAheadDays?: number +) { const { collections } = useDatabase(); const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(true); const [refreshKey, setRefreshKey] = useState(0); - const refresh = useCallback(() => setRefreshKey((k) => k + 1), []); + const refresh = useCallback(() => setRefreshKey(k => k + 1), []); const cutoff = useMemo(() => { if (maxAheadDays === undefined) return null; @@ -80,11 +81,8 @@ export function useTasks(categoryIds: string[] = [], showCompleted: boolean | 'a const conditions: any[] = []; if (categoryIds.length > 0) { - const tagConditions = categoryIds.map((id) => - Q.or( - Q.where('tags', Q.like(`%,${id},%`)), - Q.where('category_id', id) - ) + const tagConditions = categoryIds.map(id => + Q.or(Q.where('tags', Q.like(`%,${id},%`)), Q.where('category_id', id)) ); conditions.push(tagConditions.length === 1 ? tagConditions[0] : Q.or(...tagConditions)); } @@ -99,23 +97,22 @@ export function useTasks(categoryIds: string[] = [], showCompleted: boolean | 'a conditions.push(Q.where('due_date', Q.lte(cutoff))); } - const query = conditions.length > 0 - ? collections.tasks.query(Q.and(...conditions)) - : collections.tasks.query(); + const query = + conditions.length > 0 + ? collections.tasks.query(Q.and(...conditions)) + : collections.tasks.query(); - const subscription = query - .observe() - .subscribe({ - next: (result) => { - if (mounted) { - setTasks(result); - setLoading(false); - } - }, - error: () => { - if (mounted) setLoading(false); - }, - }); + const subscription = query.observe().subscribe({ + next: result => { + if (mounted) { + setTasks(result); + setLoading(false); + } + }, + error: () => { + if (mounted) setLoading(false); + }, + }); return () => { mounted = false; @@ -131,7 +128,7 @@ export function useTasksByDate(date: Date) { const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(true); const [refreshKey, setRefreshKey] = useState(0); - const refresh = useCallback(() => setRefreshKey((k) => k + 1), []); + const refresh = useCallback(() => setRefreshKey(k => k + 1), []); const startOfDay = useMemo(() => { const d = new Date(date); @@ -153,7 +150,7 @@ export function useTasksByDate(date: Date) { ) .observe() .subscribe({ - next: (result) => { + next: result => { if (mounted) { setTasks(result); setLoading(false); diff --git a/carry-your-live/src/models/Category.ts b/carry-your-live/src/models/Category.ts index a4165d5..656f44c 100644 --- a/carry-your-live/src/models/Category.ts +++ b/carry-your-live/src/models/Category.ts @@ -9,4 +9,4 @@ export default class Category extends Model { @field('order') order!: number; @date('created_at') createdAt!: Date; @date('updated_at') updatedAt!: Date; -} \ No newline at end of file +} diff --git a/carry-your-live/src/models/Friendship.ts b/carry-your-live/src/models/Friendship.ts index eb2e118..6f3db93 100644 --- a/carry-your-live/src/models/Friendship.ts +++ b/carry-your-live/src/models/Friendship.ts @@ -9,4 +9,4 @@ export default class Friendship extends Model { @field('status') status!: 'pending' | 'accepted'; @date('created_at') createdAt!: Date; @date('updated_at') updatedAt!: Date; -} \ No newline at end of file +} diff --git a/carry-your-live/src/models/Subtask.ts b/carry-your-live/src/models/Subtask.ts index a8cc919..ef6e463 100644 --- a/carry-your-live/src/models/Subtask.ts +++ b/carry-your-live/src/models/Subtask.ts @@ -34,4 +34,4 @@ export default class Subtask extends Model { @date('updated_at') updatedAt!: Date; @children('subtasks') subtasks!: any; -} \ No newline at end of file +} diff --git a/carry-your-live/src/models/Task.ts b/carry-your-live/src/models/Task.ts index 6c93388..ea9c97d 100644 --- a/carry-your-live/src/models/Task.ts +++ b/carry-your-live/src/models/Task.ts @@ -33,4 +33,4 @@ export default class Task extends Model { @date('updated_at') updatedAt!: Date; @children('subtasks') subtasks!: any; -} \ No newline at end of file +} diff --git a/carry-your-live/src/models/Tombstone.ts b/carry-your-live/src/models/Tombstone.ts index 5bd620a..6152f87 100644 --- a/carry-your-live/src/models/Tombstone.ts +++ b/carry-your-live/src/models/Tombstone.ts @@ -1,7 +1,8 @@ import { Model } from '@nozbe/watermelondb'; import { field, date } from '@nozbe/watermelondb/decorators'; -export type TombstoneEntity = 'categories' | 'tasks' | 'subtasks' | 'repeatProfiles' | 'friendships'; +export type TombstoneEntity = + 'categories' | 'tasks' | 'subtasks' | 'repeatProfiles' | 'friendships'; export default class Tombstone extends Model { static table = 'tombstones'; diff --git a/carry-your-live/src/services/auth.ts b/carry-your-live/src/services/auth.ts index a21afdb..86e26cf 100644 --- a/carry-your-live/src/services/auth.ts +++ b/carry-your-live/src/services/auth.ts @@ -148,7 +148,9 @@ export async function register(username: string, password: string): Promise { +export async function forgotPassword( + username: string +): Promise<{ success: boolean; resetToken?: string }> { const response = await apiFetch('/auth/forgot-password', { method: 'POST', body: JSON.stringify({ username }), @@ -157,7 +159,10 @@ export async function forgotPassword(username: string): Promise<{ success: boole return data; } -export async function resetPassword(token: string, password: string): Promise<{ success: boolean }> { +export async function resetPassword( + token: string, + password: string +): Promise<{ success: boolean }> { const response = await apiFetch('/auth/reset-password', { method: 'POST', body: JSON.stringify({ token, password }), diff --git a/carry-your-live/src/services/friends.ts b/carry-your-live/src/services/friends.ts index 1ab0c2f..2527d59 100644 --- a/carry-your-live/src/services/friends.ts +++ b/carry-your-live/src/services/friends.ts @@ -69,4 +69,4 @@ export async function removeFriend(friendId: string): Promise { const data = await response.json(); throw new Error(data.message || 'Failed to remove friend'); } -} \ No newline at end of file +} diff --git a/carry-your-live/src/services/notifications.ts b/carry-your-live/src/services/notifications.ts index ced0121..65eefff 100644 --- a/carry-your-live/src/services/notifications.ts +++ b/carry-your-live/src/services/notifications.ts @@ -1,6 +1,13 @@ import { Platform } from 'react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; -import { Reminder, reminderToMinutes, parseReminders, ReminderPreference, parseTaskTags } from '@/types'; +import { useSettings } from '@/theme'; +import { + Reminder, + reminderToMinutes, + parseReminders, + ReminderPreference, + parseTaskTags, +} from '@/types'; import { TagReminderPreferences, TAG_REMINDER_PREFERENCES_KEY } from '@/theme'; const ID_MAP_KEY = 'notifications:taskIdToNotificationIds'; @@ -95,7 +102,11 @@ function getRemindersForTask( return []; } -export async function scheduleTaskReminder(task: any, notificationsEnabled: boolean = true, globalReminderPreference: ReminderPreference = '15'): Promise { +export async function scheduleTaskReminder( + task: any, + notificationsEnabled: boolean = true, + globalReminderPreference: ReminderPreference = '15' +): Promise { const notifications = await getNotificationsModule(); const dueDate = task.dueDate ? new Date(task.dueDate) : null; @@ -207,7 +218,11 @@ export async function cancelAllReminders(): Promise { await writeIdMap(idMap); } -export async function rescheduleAllReminders(tasks: any[], notificationsEnabled: boolean = true, globalReminderPreference: ReminderPreference = '15'): Promise { +export async function rescheduleAllReminders( + tasks: any[], + notificationsEnabled: boolean = true, + globalReminderPreference: ReminderPreference = '15' +): Promise { if (!notificationsEnabled) { await cancelAllReminders(); return; @@ -265,3 +280,9 @@ export async function rescheduleAllReminders(tasks: any[], notificationsEnabled: await writeIdMap(idMap); } + +// Hook to get current notification settings and reschedule on change +export function useNotificationSettings() { + const { notifications, reminderPreference } = useSettings(); + return { notificationsEnabled: notifications, globalReminderPreference: reminderPreference }; +} \ No newline at end of file diff --git a/carry-your-live/src/services/pushNotifications.ts b/carry-your-live/src/services/pushNotifications.ts index 6a9b52e..8835bca 100644 --- a/carry-your-live/src/services/pushNotifications.ts +++ b/carry-your-live/src/services/pushNotifications.ts @@ -29,7 +29,9 @@ export async function registerForPushNotificationsAsync(): Promise { try { const response = await fetchWithTimeout( `/api/v1/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/latest`, - 8000, + 8000 ); if (!response.ok) return null; const release = await response.json(); const apkAsset = (release.assets ?? []).find( - (asset: { name?: string }) => asset.name && /\.apk$/i.test(asset.name), + (asset: { name?: string }) => asset.name && /\.apk$/i.test(asset.name) ); const tagName: string = release.tag_name ?? release.name ?? ''; const notes: string = release.body ?? ''; @@ -71,7 +71,7 @@ export async function fetchLatestRelease(): Promise { export function compareVersions(a: string, b: string): number { const parse = (value: string) => - value.split('.').map((part) => parseInt(part.replace(/\D/g, ''), 10) || 0); + value.split('.').map(part => parseInt(part.replace(/\D/g, ''), 10) || 0); const left = parse(a); const right = parse(b); const length = Math.max(left.length, right.length); @@ -125,8 +125,9 @@ export async function downloadAPK( url, filepath, {}, - (downloadProgress) => { - const progress = downloadProgress.totalBytesWritten / downloadProgress.totalBytesExpectedToWrite; + downloadProgress => { + const progress = + downloadProgress.totalBytesWritten / downloadProgress.totalBytesExpectedToWrite; onProgress?.(progress); } ); @@ -158,4 +159,4 @@ export async function installAPK(uri: string): Promise { export async function openReleaseUrl(url: string): Promise { const { openURL } = await import('expo-linking'); await openURL(url); -} \ No newline at end of file +} diff --git a/carry-your-live/src/theme.tsx b/carry-your-live/src/theme.tsx index d3bddbf..e790a68 100644 --- a/carry-your-live/src/theme.tsx +++ b/carry-your-live/src/theme.tsx @@ -1,6 +1,7 @@ import React, { createContext, useCallback, useContext, useEffect, useMemo, useState, ReactNode } from 'react'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { DEFAULT_API_BASE_URL, API_URL_KEY } from '@/services/auth'; +import { Platform } from 'react-native'; export type SortBy = 'date' | 'priority' | 'alpha' | 'created'; @@ -27,6 +28,8 @@ export type TagReminderPreferences = Record; export const TAG_REMINDER_PREFERENCES_KEY = 'settings:tagReminderPreferences'; +export type ThemeMode = 'light' | 'dark' | 'system'; + export interface ThemeColors { background: string; card: string; @@ -75,7 +78,6 @@ export const ACCENT_PRESETS: string[] = [ function parseHex(hex: string): [number, number, number] { const h = hex.replace(/^#/, ''); - // Handle both #RRGGBB (6 digits) and #AARRGGBB (8 digits with alpha) if (h.length === 8) { return [parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16), parseInt(h.slice(6, 8), 16)]; } @@ -83,7 +85,9 @@ function parseHex(hex: string): [number, number, number] { } function toHexByte(n: number): string { - return Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, '0'); + return Math.max(0, Math.min(255, Math.round(n))) + .toString(16) + .padStart(2, '0'); } export function mixHex(color: string, target: string, ratio: number): string { @@ -100,15 +104,48 @@ export function desaturate(hex: string, amount: number): string { } function accentTextColor(accent: string): string { - const [r, g, b] = parseHex(accent).map((v) => v / 255); + const [r, g, b] = parseHex(accent).map(v => v / 255); const linear = (v: number) => (v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4)); const luminance = 0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b); return luminance > 0.5 ? '#111111' : '#FFFFFF'; } -export function colors(accent: string = DEFAULT_ACCENT, opacity: number = 1): ThemeColors { +export function colors(accent: string = DEFAULT_ACCENT, mode: ThemeMode = 'dark', opacity: number = 1): ThemeColors { const softAccent = desaturate(accent, 0.35); const accentWithOpacity = opacity < 1 ? `${opacity.toString(16).padStart(2, '0')}${accent.slice(1)}` : accent; + + if (mode === 'light') { + return { + background: '#FFFFFF', + card: '#FAFAFA', + cardAlt: '#F0F0F0', + border: '#E0E0E0', + borderStrong: '#D0D0D0', + text: '#212121', + textSecondary: '#424242', + textFaint: '#757575', + textMuted: '#9E9E9E', + accent: opacity < 1 ? `${accentWithOpacity}` : accent, + accentStrong: mixHex(accent, '#000000', 0.55), + accentSoft: mixHex(softAccent, '#FFFFFF', 0.18), + accentBorder: mixHex(desaturate(accent, 0.15), '#FFFFFF', 0.5), + accentText: accentTextColor(accent), + inputBg: '#FFFFFF', + overlay: 'rgba(0,0,0,0.4)', + sheetBg: '#FAFAFA', + tabBarBg: '#FFFFFF', + danger: '#E53935', + dangerStrong: '#C62828', + dangerSoft: '#FBE9E7', + dangerBorder: '#FFCDD2', + success: '#43A047', + successStrong: '#2E7D32', + warning: '#FB8C00', + info: '#1E88E5', + }; + } + + // dark mode return { background: '#121212', card: '#1E1E1E', @@ -162,7 +199,11 @@ interface SettingsContextType { setShowCompleted: (value: boolean) => void; deleteBySwipe: boolean; setDeleteBySwipe: (value: boolean) => void; + themeMode: ThemeMode; + setThemeMode: (value: ThemeMode) => void; theme: ThemeColors; + syncSettingsToServer: () => Promise; + loadSettingsFromServer: () => Promise; } export const DEFAULT_TODO_AHEAD_DAYS = 7; @@ -178,17 +219,43 @@ const STORAGE_KEYS = { todoAheadDays: 'settings:todoAheadDays', showCompleted: 'settings:showCompleted', deleteBySwipe: 'settings:deleteBySwipe', + themeMode: 'settings:themeMode', }; const SettingsContext = createContext(null); +async function fetchServerSettings(apiUrl: string, token: string): Promise { + try { + const response = await fetch(`${apiUrl}/users/me/settings`, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (response.ok) return await response.json(); + } catch { + // ignore + } + return null; +} + +async function pushServerSettings(apiUrl: string, token: string, settings: any): Promise { + try { + const response = await fetch(`${apiUrl}/users/me/settings`, { + method: 'PATCH', + headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, + body: JSON.stringify(settings), + }); + return response.ok; + } catch { + return false; + } +} + function useStoredSetting(key: string, initialValue: T): [T, (value: T) => void, boolean] { const [value, setValue] = useState(initialValue); const [loaded, setLoaded] = useState(false); useEffect(() => { AsyncStorage.getItem(key) - .then((stored) => { + .then(stored => { if (stored !== null) { setValue(JSON.parse(stored) as T); } @@ -209,16 +276,22 @@ function useStoredSetting(key: string, initialValue: T): [T, (value: T) => vo } export function SettingsProvider({ children }: { children: ReactNode }) { - const [notifications, setNotifications] = useStoredSetting(STORAGE_KEYS.notifications, true); - const [defaultCategoryId, setDefaultCategoryId] = useStoredSetting(STORAGE_KEYS.defaultCategoryId, ''); + const [notifications, setNotifications, notificationsLoaded] = useStoredSetting( + STORAGE_KEYS.notifications, + true + ); + const [defaultCategoryId, setDefaultCategoryId] = useStoredSetting( + STORAGE_KEYS.defaultCategoryId, + '' + ); const [sortBy, setSortBy] = useStoredSetting(STORAGE_KEYS.sortBy, 'date'); const [reminderPreference, setReminderPreference] = useStoredSetting( STORAGE_KEYS.reminderPreference, - '15', + '15' ); const [tagReminderPreferences, setTagReminderPreferences] = useStoredSetting( STORAGE_KEYS.tagReminderPreferences, - {}, + {} ); const [apiUrl, setApiUrl] = useStoredSetting(API_URL_KEY, DEFAULT_API_BASE_URL); const [accentColor, setAccentColor] = useStoredSetting(STORAGE_KEYS.accentColor, DEFAULT_ACCENT); @@ -226,8 +299,52 @@ export function SettingsProvider({ children }: { children: ReactNode }) { const [todoAheadDays, setTodoAheadDays] = useStoredSetting(STORAGE_KEYS.todoAheadDays, DEFAULT_TODO_AHEAD_DAYS); const [showCompleted, setShowCompleted] = useStoredSetting(STORAGE_KEYS.showCompleted, false); const [deleteBySwipe, setDeleteBySwipe] = useStoredSetting(STORAGE_KEYS.deleteBySwipe, false); + const [themeMode, setThemeMode, themeModeLoaded] = useStoredSetting( + STORAGE_KEYS.themeMode, + 'system' + ); - const theme = useMemo(() => colors(accentColor), [accentColor]); + const effectiveMode = themeMode === 'system' + ? (Platform.OS === 'web' ? 'dark' : 'dark') // Will be updated by useColorScheme in real app + : themeMode; + + const theme = useMemo(() => colors(accentColor, effectiveMode, accentOpacity), [accentColor, effectiveMode, accentOpacity]); + + const syncSettingsToServer = useCallback(async () => { + const { getAuthToken } = await import('@/services/auth'); + const token = await getAuthToken(); + if (!token) return; + + const settings = { + darkMode: effectiveMode === 'dark' || (themeMode === 'system' && effectiveMode === 'dark'), + defaultCategoryId, + sortOrder: sortBy, + reminderPreference, + todoAheadDays, + showCompleted, + deleteBySwipe, + }; + await pushServerSettings(apiUrl, token, settings); + }, [apiUrl, effectiveMode, themeMode, defaultCategoryId, sortBy, reminderPreference, todoAheadDays, showCompleted, deleteBySwipe]); + + const loadSettingsFromServer = useCallback(async () => { + const { getAuthToken } = await import('@/services/auth'); + const token = await getAuthToken(); + if (!token) return; + + const serverSettings = await fetchServerSettings(apiUrl, token); + if (!serverSettings) return; + + if (serverSettings.darkMode !== undefined) { + setThemeMode(serverSettings.darkMode ? 'dark' : 'light'); + } + if (serverSettings.defaultCategoryId) setDefaultCategoryId(serverSettings.defaultCategoryId); + if (serverSettings.sortOrder) setSortBy(serverSettings.sortOrder); + if (serverSettings.reminderPreference) setReminderPreference(serverSettings.reminderPreference); + if (serverSettings.todoAheadDays !== undefined) setTodoAheadDays(serverSettings.todoAheadDays); + if (serverSettings.showCompleted !== undefined) setShowCompleted(serverSettings.showCompleted); + if (serverSettings.deleteBySwipe !== undefined) setDeleteBySwipe(serverSettings.deleteBySwipe); + }, [apiUrl, setThemeMode, setDefaultCategoryId, setSortBy, setReminderPreference, setTodoAheadDays, setShowCompleted, setDeleteBySwipe]); const value = useMemo( () => ({ @@ -253,7 +370,11 @@ export function SettingsProvider({ children }: { children: ReactNode }) { setShowCompleted, deleteBySwipe, setDeleteBySwipe, + themeMode, + setThemeMode, theme, + syncSettingsToServer, + loadSettingsFromServer, }), [ notifications, @@ -270,10 +391,19 @@ export function SettingsProvider({ children }: { children: ReactNode }) { setApiUrl, accentColor, setAccentColor, + accentOpacity, + setAccentOpacity, todoAheadDays, setTodoAheadDays, showCompleted, setShowCompleted, + deleteBySwipe, + setDeleteBySwipe, + themeMode, + setThemeMode, + theme, + syncSettingsToServer, + loadSettingsFromServer, ] ); @@ -292,4 +422,4 @@ export function useSettings() { return context; } -export * from './theme/tokens'; +export * from './theme/tokens'; \ No newline at end of file diff --git a/carry-your-live/src/theme/tokens.ts b/carry-your-live/src/theme/tokens.ts index 1a435d5..d9a990d 100644 --- a/carry-your-live/src/theme/tokens.ts +++ b/carry-your-live/src/theme/tokens.ts @@ -191,4 +191,4 @@ export const Z_INDEX = { modal: 200, toast: 300, tooltip: 400, -} as const; \ No newline at end of file +} as const; diff --git a/carry-your-live/src/types/index.ts b/carry-your-live/src/types/index.ts index f5a2f77..d283b2c 100644 --- a/carry-your-live/src/types/index.ts +++ b/carry-your-live/src/types/index.ts @@ -17,7 +17,7 @@ export const WEEKDAY_LABELS = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; export const WEEKDAY_ORDER = [1, 2, 3, 4, 5, 6, 0]; export function weekdaysInDisplayOrder(days: number[]): number[] { - return WEEKDAY_ORDER.filter((day) => days.includes(day)); + return WEEKDAY_ORDER.filter(day => days.includes(day)); } export function repeatDaysToString(days: number[]): string { @@ -28,7 +28,7 @@ export function repeatDaysFromString(raw: string): number[] { return (raw || '') .split(',') .map(Number) - .filter((d) => !Number.isNaN(d)); + .filter(d => !Number.isNaN(d)); } export interface RepeatProfileData { @@ -53,7 +53,7 @@ export const REMINDER_OPTIONS: { value: Reminder; label: string }[] = [ { value: '1440', label: '1 day before' }, ]; -export const MULTIPLE_REMINDER_OPTIONS = REMINDER_OPTIONS.filter((o) => o.value !== 'none'); +export const MULTIPLE_REMINDER_OPTIONS = REMINDER_OPTIONS.filter(o => o.value !== 'none'); export function reminderToMinutes(reminder: Reminder): number | null { if (reminder === 'none') return null; @@ -86,12 +86,12 @@ export function parseReminders(reminders: string): Reminder[] { if (!reminders) return []; return reminders .split(',') - .map((r) => r.trim() as Reminder) + .map(r => r.trim() as Reminder) .filter((r): r is Exclude => r !== 'none' && Boolean(r)); } export function toRemindersString(reminders: Reminder[]): string { - return reminders.filter((r) => r !== 'none').join(','); + return reminders.filter(r => r !== 'none').join(','); } export interface CategoryData { @@ -109,7 +109,7 @@ export function tagsFromString(raw: string | null | undefined): string[] { if (!raw) return []; return raw .split(',') - .map((t) => t.trim()) + .map(t => t.trim()) .filter(Boolean); } @@ -209,4 +209,4 @@ export interface SubtaskFormData { reminders: string; assigneeId: string | null; parentSubtaskId?: string | null; -} \ No newline at end of file +} diff --git a/carry-your-live/src/utils/categoryActions.ts b/carry-your-live/src/utils/categoryActions.ts index 330ef0b..fe49465 100644 --- a/carry-your-live/src/utils/categoryActions.ts +++ b/carry-your-live/src/utils/categoryActions.ts @@ -8,7 +8,7 @@ export async function createCategory(name: string, color: string): Promise const existing = await collections.categories.query().fetch(); const nextOrder = existing.reduce((max, c) => Math.max(max, c.order), -1) + 1; const now = new Date(); - await collections.categories.create((c) => { + await collections.categories.create(c => { c.name = name; c.color = color; c.order = nextOrder; @@ -18,10 +18,14 @@ export async function createCategory(name: string, color: string): Promise }); } -export async function updateCategory(categoryId: string, name: string, color: string): Promise { +export async function updateCategory( + categoryId: string, + name: string, + color: string +): Promise { await database.write(async () => { const category = await collections.categories.find(categoryId); - await category.update((c) => { + await category.update(c => { c.name = name; c.color = color; c.updatedAt = new Date(); @@ -31,23 +35,25 @@ export async function updateCategory(categoryId: string, name: string, color: st export async function deleteCategory(categoryId: string): Promise { await database.write(async () => { - const others = (await collections.categories.query().fetch()).filter((c) => c.id !== categoryId); + const others = (await collections.categories.query().fetch()).filter(c => c.id !== categoryId); const fallback = others[0]; const tasks = await collections.tasks.query(Q.where('category_id', categoryId)).fetch(); for (const task of tasks) { - await task.update((t) => { - const tags = parseTaskTags(t.tags, t.categoryId).filter((id) => id !== categoryId); + await task.update(t => { + const tags = parseTaskTags(t.tags, t.categoryId).filter(id => id !== categoryId); t.categoryId = fallback?.id ?? tags[0] ?? ''; t.tags = tagsToString(tags); t.updatedAt = new Date(); }); } - const tagReferencedTasks = await collections.tasks.query(Q.where('tags', Q.like(`%,${categoryId},%`))).fetch(); + const tagReferencedTasks = await collections.tasks + .query(Q.where('tags', Q.like(`%,${categoryId},%`))) + .fetch(); for (const task of tagReferencedTasks) { - const tags = parseTaskTags(task.tags, task.categoryId).filter((id) => id !== categoryId); - await task.update((t) => { + const tags = parseTaskTags(task.tags, task.categoryId).filter(id => id !== categoryId); + await task.update(t => { t.tags = tagsToString(tags); t.updatedAt = new Date(); }); diff --git a/carry-your-live/src/utils/confirm.ts b/carry-your-live/src/utils/confirm.ts index 07c956b..40414e5 100644 --- a/carry-your-live/src/utils/confirm.ts +++ b/carry-your-live/src/utils/confirm.ts @@ -8,7 +8,13 @@ type ConfirmOptions = { onConfirm: () => void; }; -export function confirmAction({ title, message, confirmLabel = 'Delete', destructive, onConfirm }: ConfirmOptions): void { +export function confirmAction({ + title, + message, + confirmLabel = 'Delete', + destructive, + onConfirm, +}: ConfirmOptions): void { if (Platform.OS === 'web') { const confirmed = (globalThis as any).confirm(message ? `${title}\n\n${message}` : title); if (confirmed) onConfirm(); diff --git a/carry-your-live/src/utils/icalParser.ts b/carry-your-live/src/utils/icalParser.ts index 30caaf4..ed5a3a1 100644 --- a/carry-your-live/src/utils/icalParser.ts +++ b/carry-your-live/src/utils/icalParser.ts @@ -153,4 +153,4 @@ function parseICalDateTime(dateStr: string): Date { return new Date(dateStr); } -export default parseICal; \ No newline at end of file +export default parseICal; diff --git a/carry-your-live/src/utils/quickAddFocus.ts b/carry-your-live/src/utils/quickAddFocus.ts index 88d21ed..d39950b 100644 --- a/carry-your-live/src/utils/quickAddFocus.ts +++ b/carry-your-live/src/utils/quickAddFocus.ts @@ -5,7 +5,7 @@ let pending = false; export function requestQuickAddFocus(): void { pending = true; - listeners.forEach((listener) => listener()); + listeners.forEach(listener => listener()); } export function subscribeToQuickAdd(listener: QuickAddListener): () => void { diff --git a/carry-your-live/src/utils/repeatProfileActions.ts b/carry-your-live/src/utils/repeatProfileActions.ts index 5b9c727..42c4c8d 100644 --- a/carry-your-live/src/utils/repeatProfileActions.ts +++ b/carry-your-live/src/utils/repeatProfileActions.ts @@ -10,7 +10,7 @@ export async function createRepeatProfile( ): Promise { const now = new Date(); await database.write(async () => { - await collections.repeatProfiles.create((p) => { + await collections.repeatProfiles.create(p => { p.name = name.trim(); p.repeat = repeat; p.repeatInterval = interval || 1; diff --git a/carry-your-live/src/utils/tabHistory.ts b/carry-your-live/src/utils/tabHistory.ts index bd57121..f55e26b 100644 --- a/carry-your-live/src/utils/tabHistory.ts +++ b/carry-your-live/src/utils/tabHistory.ts @@ -13,4 +13,4 @@ export function getLastVisitedTab(): string { export function tabHref(tab: string): string { if (tab === 'index') return '/'; return `/${tab}`; -} \ No newline at end of file +} diff --git a/carry-your-live/src/utils/taskActions.ts b/carry-your-live/src/utils/taskActions.ts index fac9c93..3d17bc5 100644 --- a/carry-your-live/src/utils/taskActions.ts +++ b/carry-your-live/src/utils/taskActions.ts @@ -32,12 +32,16 @@ function mapSubtaskRow(s: any, taskId?: string): SubtaskData { } export async function fetchSubtaskTree(taskId: string): Promise { - const topLevel = await collections.subtasks.query(Q.where('task_id', taskId), Q.where('parent_subtask_id', null)).fetch(); + const topLevel = await collections.subtasks + .query(Q.where('task_id', taskId), Q.where('parent_subtask_id', null)) + .fetch(); const items: SubtaskData[] = topLevel.map((s: any) => mapSubtaskRow(s, taskId)); const decorate = async (subtasks: SubtaskData[]): Promise => { for (const sub of subtasks) { - const children = await collections.subtasks.query(Q.where('parent_subtask_id', sub.id)).fetch(); + const children = await collections.subtasks + .query(Q.where('parent_subtask_id', sub.id)) + .fetch(); if (children.length > 0) { sub.subtasks = await decorate(children.map((c: any) => mapSubtaskRow(c, taskId))); } @@ -70,7 +74,12 @@ function startOfDay(date: Date): Date { return d; } -function nextSelectedDay(base: Date, days: number[], intervalWeeks: number, todayStart: Date): Date { +function nextSelectedDay( + base: Date, + days: number[], + intervalWeeks: number, + todayStart: Date +): Date { const msDay = 86400000; for (let offset = 1; offset <= intervalWeeks * 7 + 7; offset++) { @@ -95,8 +104,8 @@ export function parseRepeatDays(repeatDays: string): number[] { if (!repeatDays) return []; return repeatDays .split(',') - .map((d) => parseInt(d, 10)) - .filter((d) => !Number.isNaN(d) && d >= 0 && d <= 6); + .map(d => parseInt(d, 10)) + .filter(d => !Number.isNaN(d) && d >= 0 && d <= 6); } export function computeNextOccurrence( @@ -152,7 +161,10 @@ export async function deleteTaskWithSubtasks(taskId: string): Promise { const task = await collections.tasks.find(taskId); await task.destroyPermanently(); if (subtasks.length > 0) { - await recordTombstonesInBatch('subtasks', subtasks.map((s) => s.id)); + await recordTombstonesInBatch( + 'subtasks', + subtasks.map(s => s.id) + ); } await recordTombstonesInBatch('tasks', [taskId]); } @@ -171,7 +183,7 @@ export async function deleteTaskOccurrences(taskId: string, scope: TaskDeleteSco const seriesTasks = await collections.tasks.query(Q.where('series_id', task.seriesId)).fetch(); const anchor = task.dueDate || task.createdAt.getTime(); - const toDelete = seriesTasks.filter((t) => { + const toDelete = seriesTasks.filter(t => { if (scope === 'all') return true; return (t.dueDate || t.createdAt.getTime()) >= anchor; }); @@ -186,14 +198,16 @@ export async function deleteTaskOccurrences(taskId: string, scope: TaskDeleteSco }); } -export async function getSeriesOccurrenceCounts(taskId: string): Promise<{ future: number; all: number }> { +export async function getSeriesOccurrenceCounts( + taskId: string +): Promise<{ future: number; all: number }> { const task = await collections.tasks.find(taskId); if (!task.seriesId || task.repeat === 'none') { return { future: 1, all: 1 }; } const seriesTasks = await collections.tasks.query(Q.where('series_id', task.seriesId)).fetch(); const anchor = task.dueDate || task.createdAt.getTime(); - const future = seriesTasks.filter((t) => (t.dueDate || t.createdAt.getTime()) >= anchor).length; + const future = seriesTasks.filter(t => (t.dueDate || t.createdAt.getTime()) >= anchor).length; return { future, all: seriesTasks.length }; } @@ -202,7 +216,7 @@ export async function toggleTaskComplete(taskId: string): Promise await database.write(async () => { const task = await collections.tasks.find(taskId); const completing = !task.completed; - await task.update((t) => { + await task.update(t => { t.completed = completing; t.completedAt = completing ? Date.now() : null; t.updatedAt = new Date(); @@ -219,10 +233,9 @@ export async function toggleTaskComplete(taskId: string): Promise } export async function countOpenSubtasks(taskId: string): Promise { - const open = await collections.subtasks.query( - Q.where('task_id', taskId), - Q.where('completed', false) - ).fetch(); + const open = await collections.subtasks + .query(Q.where('task_id', taskId), Q.where('completed', false)) + .fetch(); return open.length; } @@ -266,12 +279,12 @@ export async function confirmAndToggleTaskComplete( export async function toggleSubtaskComplete(subtaskId: string): Promise { await database.write(async () => { const subtask = await collections.subtasks.find(subtaskId); - await subtask.update((s) => { + await subtask.update(s => { s.completed = !s.completed; s.updatedAt = new Date(); }); const task = await collections.tasks.find(subtask.taskId); - await task.update((t) => { + await task.update(t => { t.updatedAt = new Date(); }); }); @@ -300,7 +313,7 @@ export interface SubtaskUpdateData { export async function updateSubtask(subtaskId: string, data: SubtaskUpdateData): Promise { await database.write(async () => { const subtask = await collections.subtasks.find(subtaskId); - await subtask.update((s) => { + await subtask.update(s => { s.title = data.title.trim(); s.description = data.description || ''; if (data.categoryId !== undefined) s.categoryId = data.categoryId; @@ -318,7 +331,7 @@ export async function updateSubtask(subtaskId: string, data: SubtaskUpdateData): s.updatedAt = new Date(); }); const task = await collections.tasks.find(subtask.taskId); - await task.update((t) => { + await task.update(t => { t.updatedAt = new Date(); }); }); @@ -332,9 +345,11 @@ export async function deleteSubtask(subtaskId: string): Promise { await database.write(async () => { const subtask = await collections.subtasks.find(subtaskId); parentTaskId = subtask.taskId; - const children = await collections.subtasks.query(Q.where('parent_subtask_id', subtaskId)).fetch(); + const children = await collections.subtasks + .query(Q.where('parent_subtask_id', subtaskId)) + .fetch(); for (const child of children) { - await child.update((c) => { + await child.update(c => { c.parentSubtaskId = null; c.updatedAt = new Date(); }); @@ -347,7 +362,7 @@ export async function deleteSubtask(subtaskId: string): Promise { try { await database.write(async () => { const task = await collections.tasks.find(parentTaskId!); - await task.update((t) => { + await task.update(t => { t.updatedAt = new Date(); }); }); @@ -364,7 +379,7 @@ export async function duplicateSubtask(subtaskId: string): Promise { const siblings = await collections.subtasks.query(Q.where('task_id', subtask.taskId)).fetch(); const now = new Date(); - clone = await collections.subtasks.create((s) => { + clone = await collections.subtasks.create(s => { s.taskId = subtask.taskId; s.categoryId = subtask.categoryId || ''; s.tags = subtask.tags || ''; @@ -388,7 +403,7 @@ export async function duplicateSubtask(subtaskId: string): Promise { }); const task = await collections.tasks.find(subtask.taskId); - await task.update((t) => { + await task.update(t => { t.updatedAt = new Date(); }); }); @@ -398,18 +413,21 @@ export async function duplicateSubtask(subtaskId: string): Promise { export async function setSubtaskPriority(subtaskId: string, priority: Priority): Promise { await database.write(async () => { const subtask = await collections.subtasks.find(subtaskId); - await subtask.update((s) => { + await subtask.update(s => { s.priority = priority; s.updatedAt = new Date(); }); const task = await collections.tasks.find(subtask.taskId); - await task.update((t) => { + await task.update(t => { t.updatedAt = new Date(); }); }); } -export async function undoTaskComplete(taskId: string, nextOccurrenceId: string | null): Promise { +export async function undoTaskComplete( + taskId: string, + nextOccurrenceId: string | null +): Promise { await database.write(async () => { if (nextOccurrenceId) { try { @@ -423,7 +441,7 @@ export async function undoTaskComplete(taskId: string, nextOccurrenceId: string try { const task = await collections.tasks.find(taskId); if (task.completed) { - await task.update((t) => { + await task.update(t => { t.completed = false; t.updatedAt = new Date(); }); @@ -446,19 +464,27 @@ export async function deleteTask(taskId: string): Promise { }); } -export async function convertTaskToSubtask(taskId: string, parentTaskId: string, parentSubtaskId: string | null = null): Promise { +export async function convertTaskToSubtask( + taskId: string, + parentTaskId: string, + parentSubtaskId: string | null = null +): Promise { if (taskId === parentTaskId) return; await database.write(async () => { const task = await collections.tasks.find(taskId); const ownSubtasks = await collections.subtasks.query(Q.where('task_id', taskId)).fetch(); - const existing = await collections.subtasks.query( - Q.where('task_id', parentTaskId), - parentSubtaskId ? Q.where('parent_subtask_id', parentSubtaskId) : Q.where('parent_subtask_id', null) - ).fetch(); + const existing = await collections.subtasks + .query( + Q.where('task_id', parentTaskId), + parentSubtaskId + ? Q.where('parent_subtask_id', parentSubtaskId) + : Q.where('parent_subtask_id', null) + ) + .fetch(); const now = new Date(); let order = existing.length; - const created = await collections.subtasks.create((s) => { + const created = await collections.subtasks.create(s => { s.taskId = parentTaskId; s.parentSubtaskId = parentSubtaskId; s.categoryId = task.categoryId || ''; @@ -482,7 +508,7 @@ export async function convertTaskToSubtask(taskId: string, parentTaskId: string, }); for (const subtask of ownSubtasks) { - await collections.subtasks.create((s) => { + await collections.subtasks.create(s => { s.taskId = parentTaskId; s.parentSubtaskId = created.id; s.categoryId = subtask.categoryId || ''; @@ -507,7 +533,7 @@ export async function convertTaskToSubtask(taskId: string, parentTaskId: string, } const parent = await collections.tasks.find(parentTaskId); - await parent.update((p) => { + await parent.update(p => { p.updatedAt = now; }); @@ -524,11 +550,13 @@ export async function convertSubtaskToTask(subtaskId: string): Promise { const now = new Date(); const subTags = parseTaskTags(subtask.tags, subtask.categoryId); - const task = await collections.tasks.create((t) => { + const task = await collections.tasks.create(t => { t.title = subtask.title; t.description = subtask.description || ''; t.categoryId = subTags[0] || parent.categoryId || ''; - t.tags = tagsToString(subTags.length > 0 ? subTags : (parent.categoryId ? [parent.categoryId] : [])); + t.tags = tagsToString( + subTags.length > 0 ? subTags : parent.categoryId ? [parent.categoryId] : [] + ); t.priority = subtask.priority; t.completed = subtask.completed; t.dueDate = subtask.dueDate; @@ -546,9 +574,11 @@ export async function convertSubtaskToTask(subtaskId: string): Promise { }); newTaskId = task.id; - const children = await collections.subtasks.query(Q.where('parent_subtask_id', subtaskId)).fetch(); + const children = await collections.subtasks + .query(Q.where('parent_subtask_id', subtaskId)) + .fetch(); for (const child of children) { - await child.update((c) => { + await child.update(c => { c.parentSubtaskId = null; c.updatedAt = now; }); @@ -556,7 +586,7 @@ export async function convertSubtaskToTask(subtaskId: string): Promise { await subtask.destroyPermanently(); await recordTombstonesInBatch('subtasks', [subtaskId]); - await parent.update((p) => { + await parent.update(p => { p.updatedAt = now; }); }); @@ -576,7 +606,7 @@ export async function moveSubtaskToTask(subtaskId: string, toTaskId: string): Pr const siblings = await collections.subtasks.query(Q.where('task_id', toTaskId)).fetch(); const now = new Date(); - await subtask.update((s) => { + await subtask.update(s => { s.taskId = toTaskId; s.parentSubtaskId = null; s.order = siblings.length; @@ -584,14 +614,14 @@ export async function moveSubtaskToTask(subtaskId: string, toTaskId: string): Pr }); try { const oldParent = await collections.tasks.find(oldParentId); - await oldParent.update((p) => { + await oldParent.update(p => { p.updatedAt = now; }); } catch { // parent already gone } const newParent = await collections.tasks.find(toTaskId); - await newParent.update((p) => { + await newParent.update(p => { p.updatedAt = now; }); }); @@ -617,19 +647,18 @@ export async function setSubtaskParent(subtaskId: string, newParentId: string): cursor = await collections.subtasks.find(cursor.parentSubtaskId); } - const siblings = await collections.subtasks.query( - Q.where('task_id', subtask.taskId), - Q.where('parent_subtask_id', newParentId) - ).fetch(); + const siblings = await collections.subtasks + .query(Q.where('task_id', subtask.taskId), Q.where('parent_subtask_id', newParentId)) + .fetch(); - await subtask.update((s) => { + await subtask.update(s => { s.parentSubtaskId = newParentId; s.order = siblings.length; s.updatedAt = now; }); const task = await collections.tasks.find(subtask.taskId); - await task.update((t) => { + await task.update(t => { t.updatedAt = now; }); }); @@ -639,7 +668,7 @@ export async function setTaskCompleted(taskId: string, completed: boolean): Prom let nextOccurrence: any = null; await database.write(async () => { const task = await collections.tasks.find(taskId); - await task.update((t) => { + await task.update(t => { t.completed = completed; t.completedAt = completed ? Date.now() : null; t.updatedAt = new Date(); @@ -656,10 +685,9 @@ export async function setTaskCompleted(taskId: string, completed: boolean): Prom async function createNextOccurrenceIfNeeded(task: any): Promise { const seriesId = task.seriesId || newSeriesId(); - const existing = await collections.tasks.query( - Q.where('series_id', seriesId), - Q.where('completed', false) - ).fetch(); + const existing = await collections.tasks + .query(Q.where('series_id', seriesId), Q.where('completed', false)) + .fetch(); if (existing.length > 0) return null; const next = await createNextOccurrence(task, seriesId); if (!task.seriesId) { @@ -671,11 +699,16 @@ async function createNextOccurrenceIfNeeded(task: any): Promise { } async function createNextOccurrence(task: any, seriesId: string): Promise { - const nextDate = computeNextOccurrence(task.dueDate, task.repeat, task.repeatInterval, task.repeatDays); + const nextDate = computeNextOccurrence( + task.dueDate, + task.repeat, + task.repeatInterval, + task.repeatDays + ); const subtasks = await collections.subtasks.query(Q.where('task_id', task.id)).fetch(); const now = new Date(); - const next = await collections.tasks.create((t) => { + const next = await collections.tasks.create(t => { t.title = task.title; t.description = task.description; t.categoryId = task.categoryId; @@ -697,7 +730,7 @@ async function createNextOccurrence(task: any, seriesId: string): Promise { }); for (const subtask of subtasks) { - await collections.subtasks.create((s) => { + await collections.subtasks.create(s => { s.taskId = next.id; s.title = subtask.title; s.description = subtask.description; @@ -723,7 +756,7 @@ async function createNextOccurrence(task: any, seriesId: string): Promise { export async function setTaskCategory(taskId: string, categoryId: string): Promise { await database.write(async () => { const task = await collections.tasks.find(taskId); - await task.update((t) => { + await task.update(t => { t.categoryId = categoryId; t.tags = tagsToString(categoryId ? [categoryId] : []); t.updatedAt = new Date(); @@ -734,7 +767,7 @@ export async function setTaskCategory(taskId: string, categoryId: string): Promi export async function setTaskPriority(taskId: string, priority: Priority): Promise { await database.write(async () => { const task = await collections.tasks.find(taskId); - await task.update((t) => { + await task.update(t => { t.priority = priority; t.updatedAt = new Date(); }); @@ -748,7 +781,7 @@ export async function duplicateTask(taskId: string): Promise { const subtasks = await collections.subtasks.query(Q.where('task_id', taskId)).fetch(); const now = new Date(); - clone = await collections.tasks.create((t) => { + clone = await collections.tasks.create(t => { t.title = task.title; t.description = task.description; t.categoryId = task.categoryId; @@ -770,7 +803,7 @@ export async function duplicateTask(taskId: string): Promise { }); for (const subtask of subtasks) { - await collections.subtasks.create((s) => { + await collections.subtasks.create(s => { s.taskId = clone.id; s.title = subtask.title; s.description = subtask.description; @@ -796,7 +829,7 @@ export async function reorderTasks(taskIds: string[]): Promise { const now = new Date(); for (let i = 0; i < taskIds.length; i++) { const task = await collections.tasks.find(taskIds[i]); - await task.update((t) => { + await task.update(t => { t.order = i; t.updatedAt = now; }); @@ -809,7 +842,7 @@ export async function reorderSubtasks(subtaskIds: string[]): Promise { const now = new Date(); for (let i = 0; i < subtaskIds.length; i++) { const subtask = await collections.subtasks.find(subtaskIds[i]); - await subtask.update((s) => { + await subtask.update(s => { s.order = i; s.updatedAt = now; }); @@ -840,12 +873,16 @@ export async function createSubtask(data: CreateSubtaskData): Promise { let newSubtaskId: string | null = null; await database.write(async () => { const now = new Date(); - const siblings = await collections.subtasks.query( - Q.where('task_id', data.taskId), - data.parentSubtaskId ? Q.where('parent_subtask_id', data.parentSubtaskId) : Q.where('parent_subtask_id', null) - ).fetch(); + const siblings = await collections.subtasks + .query( + Q.where('task_id', data.taskId), + data.parentSubtaskId + ? Q.where('parent_subtask_id', data.parentSubtaskId) + : Q.where('parent_subtask_id', null) + ) + .fetch(); - const subtask = await collections.subtasks.create((s) => { + const subtask = await collections.subtasks.create(s => { s.taskId = data.taskId; s.parentSubtaskId = data.parentSubtaskId || null; s.categoryId = data.categoryId || ''; diff --git a/carry-your-live/tsconfig.json b/carry-your-live/tsconfig.json index cb3caf4..a150c2e 100644 --- a/carry-your-live/tsconfig.json +++ b/carry-your-live/tsconfig.json @@ -26,4 +26,4 @@ "ignoreDeprecations": "6.0" }, "exclude": ["node_modules"] -} \ No newline at end of file +}