diff --git a/carry-your-live/app/(tabs)/calendar.tsx b/carry-your-live/app/(tabs)/calendar.tsx
index 8a11014..4bc56bc 100644
--- a/carry-your-live/app/(tabs)/calendar.tsx
+++ b/carry-your-live/app/(tabs)/calendar.tsx
@@ -1,224 +1,448 @@
-import React, { useMemo, useRef, useState, useCallback } from 'react';
-import { View, Text, StyleSheet, SafeAreaView, ScrollView, TouchableOpacity, FlatList } from 'react-native';
+import React, { useMemo, useRef, useState, useCallback, useEffect } from 'react';
+import { View, Text, StyleSheet, SafeAreaView, ScrollView, TouchableOpacity, Animated, Dimensions } from 'react-native';
import { useRouter } from 'expo-router';
+import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { Header } from '@/components/Header';
-import { useTasksByDate } from '@/hooks/useTasks';
+import { useTasksByDate, useTasksInMonth } from '@/hooks/useTasks';
import { useTaskModals } from '@/hooks/useTaskModals';
-import { toggleTaskComplete } from '@/utils/taskActions';
import { useSettings } from '@/theme';
-import { TaskItem } from '@/components/TaskItem';
+import { useCategories, useDatabase } from '@/hooks/useDatabase';
+import { toggleTaskComplete } from '@/utils/taskActions';
import { QuickAddBar } from '@/components/QuickAddBar';
-import { TaskData } from '@/types';
-import { format, startOfMonth, endOfMonth, eachDayOfInterval, isSameDay, isSameMonth, addMonths, isToday, startOfDay } from 'date-fns';
-import Svg, { Path } from 'react-native-svg';
+import { OptionPickerModal } from '@/components/OptionPickerModal';
+import { SubtaskData } from '@/types';
+import Task from '@/models/Task';
+import { format, addMonths, addDays, startOfMonth, isSameDay, isSameMonth, isToday } from 'date-fns';
+import { Q } from '@nozbe/watermelondb';
+import Svg, { Path, Circle } from 'react-native-svg';
import type { ThemeColors } from '@/theme';
-const DAY_WIDTH = 44;
-const DAY_GAP = 6;
+const WIDTH = Dimensions.get('window').width;
+const ORANGE = '#FF7043';
+const GERMAN_WEEKDAYS = ['mo', 'di', 'mi', 'do', 'fr', 'sa', 'so'];
+const MONTH_NAMES = [
+ 'January', 'February', 'March', 'April', 'May', 'June',
+ 'July', 'August', 'September', 'October', 'November', 'December',
+];
export default function CalendarScreen() {
const router = useRouter();
const { theme } = useSettings();
- const { modals, openTaskMenu, openTaskDelete } = useTaskModals();
+ const { collections } = useDatabase();
+ const categories = useCategories();
+ const { modals, openTaskMenu } = useTaskModals();
+
const [visibleMonth, setVisibleMonth] = useState(() => new Date());
const [selectedDate, setSelectedDate] = useState(() => new Date());
- const stripRef = useRef(null);
+ const [monthPickerVisible, setMonthPickerVisible] = useState(false);
+ const [yearPickerVisible, setYearPickerVisible] = useState(false);
+ const [subtasksMap, setSubtasksMap] = useState>({});
- const days = useMemo(
- () => eachDayOfInterval({ start: startOfMonth(visibleMonth), end: endOfMonth(visibleMonth) }),
- [visibleMonth]
+ const visibleMonthRef = useRef(visibleMonth);
+ visibleMonthRef.current = visibleMonth;
+ const selectedDateRef = useRef(selectedDate);
+ selectedDateRef.current = selectedDate;
+
+ const translateX = useRef(new Animated.Value(0)).current;
+ const animatingRef = useRef(false);
+ const gridWidthRef = useRef(WIDTH);
+
+ const { tasks: selectedDayTasks } = useTasksByDate(selectedDate);
+ const monthTasks = useTasksInMonth(visibleMonth);
+
+ const weeks = useMemo(() => {
+ const first = startOfMonth(visibleMonth);
+ const offset = (first.getDay() + 6) % 7; // week starts Monday
+ const gridStart = addDays(first, -offset);
+ const cells = Array.from({ length: 42 }, (_, i) => addDays(gridStart, i));
+ const rows: Date[][] = [];
+ for (let i = 0; i < 42; i += 7) rows.push(cells.slice(i, i + 7));
+ return rows;
+ }, [visibleMonth]);
+
+ const loadSubtasks = useCallback(
+ async (taskId: string): Promise<[string, SubtaskData[]]> => {
+ const subs = await collections.subtasks
+ .query(Q.where('task_id', taskId), Q.where('parent_subtask_id', null), Q.sortBy('order', 'asc'))
+ .fetch();
+ const mapped: SubtaskData[] = subs.map((s: any) => ({
+ id: s.id,
+ taskId: s.taskId,
+ parentSubtaskId: s.parentSubtaskId || null,
+ title: s.title,
+ description: s.description || '',
+ priority: (s.priority || 'none') as SubtaskData['priority'],
+ completed: s.completed,
+ dueDate: s.dueDate || 0,
+ dueTime: s.dueTime || '',
+ endTime: s.endTime || '',
+ allDay: s.allDay ?? false,
+ repeat: (s.repeat || 'none') as SubtaskData['repeat'],
+ repeatInterval: s.repeatInterval ?? 1,
+ repeatDays: s.repeatDays || '',
+ seriesId: s.seriesId || '',
+ reminder: (s.reminder || 'none') as SubtaskData['reminder'],
+ assigneeId: s.assigneeId ?? null,
+ order: s.order,
+ subtasks: [],
+ }));
+ return [taskId, mapped];
+ },
+ [collections.subtasks]
);
- const { tasks, loading } = useTasksByDate(selectedDate);
+ useEffect(() => {
+ let active = true;
+ setSubtasksMap({});
+ Promise.all(selectedDayTasks.map((t) => loadSubtasks(t.id))).then((entries) => {
+ if (!active) return;
+ setSubtasksMap(Object.fromEntries(entries));
+ });
+ return () => {
+ active = false;
+ };
+ }, [selectedDayTasks, loadSubtasks]);
const handleDayPress = useCallback((day: Date) => {
setSelectedDate(day);
- if (!isSameMonth(day, visibleMonth)) {
+ if (!isSameMonth(day, visibleMonthRef.current)) {
setVisibleMonth(day);
}
- }, [visibleMonth]);
+ }, []);
- const handlePrevMonth = useCallback(() => {
- const prev = addMonths(visibleMonth, -1);
- setVisibleMonth(prev);
- if (!isSameMonth(selectedDate, prev)) {
- setSelectedDate(startOfMonth(prev));
- }
- }, [visibleMonth, selectedDate]);
+ const transitionTo = useCallback(
+ (dir: 1 | -1) => {
+ if (animatingRef.current) return;
+ animatingRef.current = true;
+ const w = gridWidthRef.current || WIDTH;
+ const target = dir === 1 ? -w : w;
+ Animated.timing(translateX, { toValue: target, duration: 220, useNativeDriver: false }).start(() => {
+ const next = addMonths(visibleMonthRef.current, dir);
+ if (!isSameMonth(selectedDateRef.current, next)) {
+ setSelectedDate(startOfMonth(next));
+ }
+ setVisibleMonth(next);
+ translateX.setValue(-target);
+ Animated.timing(translateX, { toValue: 0, duration: 180, useNativeDriver: false }).start(() => {
+ animatingRef.current = false;
+ });
+ });
+ },
+ [translateX]
+ );
- const handleNextMonth = useCallback(() => {
- const next = addMonths(visibleMonth, 1);
- setVisibleMonth(next);
- if (!isSameMonth(selectedDate, next)) {
- setSelectedDate(startOfMonth(next));
- }
- }, [visibleMonth, selectedDate]);
+ const pan = useMemo(
+ () =>
+ Gesture.Pan()
+ .activeOffsetX([-16, 16])
+ .minDistance(6)
+ .runOnJS(true)
+ .onUpdate((e) => {
+ if (!animatingRef.current) translateX.setValue(e.translationX);
+ })
+ .onEnd((e) => {
+ if (animatingRef.current) return;
+ const w = gridWidthRef.current || WIDTH;
+ const dx = e.translationX;
+ if (dx <= -w / 4) {
+ translateX.stopAnimation();
+ transitionTo(1);
+ } else if (dx >= w / 4) {
+ translateX.stopAnimation();
+ transitionTo(-1);
+ } else {
+ Animated.spring(translateX, { toValue: 0, useNativeDriver: false }).start();
+ }
+ }),
+ [translateX, transitionTo]
+ );
+
+ const handleSelectMonth = useCallback((value: string | string[]) => {
+ const monthIndex = parseInt(Array.isArray(value) ? value[0] : value, 10);
+ setVisibleMonth(new Date(visibleMonthRef.current.getFullYear(), monthIndex, 1));
+ }, []);
+
+ const handleSelectYear = useCallback((value: string | string[]) => {
+ const year = parseInt(Array.isArray(value) ? value[0] : value, 10);
+ setVisibleMonth(new Date(year, visibleMonthRef.current.getMonth(), 1));
+ }, []);
+
+ const currentYear = new Date().getFullYear();
+ 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) });
+ return options;
+ }, [currentYear]);
const handleToggleComplete = useCallback(async (taskId: string) => {
await toggleTaskComplete(taskId);
}, []);
- const scrollToDay = useCallback((day: Date) => {
- const index = days.findIndex((d) => isSameDay(d, day));
- if (index >= 0) {
- stripRef.current?.scrollTo({ x: Math.max(0, index * (DAY_WIDTH + DAY_GAP) - 24), animated: true });
- }
- }, [days]);
-
- React.useEffect(() => {
- const target = isSameMonth(selectedDate, visibleMonth) ? selectedDate : startOfDay(new Date());
- scrollToDay(target);
- }, [visibleMonth, scrollToDay, selectedDate]);
-
- const renderTask = useCallback(
- ({ item }: { item: TaskData }) => (
- handleToggleComplete(item.id)}
- onDelete={() => openTaskDelete(item)}
- onPress={() => router.push({ pathname: '/task-detail', params: { id: item.id } })}
- onMenuOpen={() => openTaskMenu(item)}
- />
- ),
- [handleToggleComplete, openTaskDelete, openTaskMenu, router]
- );
-
return (
-
+
+
+
-
- {days.map((day) => (
-
+ {
+ gridWidthRef.current = e.nativeEvent.layout.width;
+ }}
+ style={[styles.calendarArea, { transform: [{ translateX }] }]}
+ >
+
+ transitionTo(-1)}
+ style={[styles.monthNav, { borderColor: theme.borderStrong, backgroundColor: theme.card }]}
+ activeOpacity={0.7}
+ >
+
+
+
+
+ setMonthPickerVisible(true)} activeOpacity={0.7} style={styles.monthButton}>
+ {format(visibleMonth, 'MMMM')}
+
+
+ setYearPickerVisible(true)} activeOpacity={0.7} style={styles.yearButton}>
+ {format(visibleMonth, 'yyyy')}
+
+
+
+ transitionTo(1)}
+ style={[styles.monthNav, { borderColor: theme.borderStrong, backgroundColor: theme.card }]}
+ activeOpacity={0.7}
+ >
+
+
+
+
+
+ {GERMAN_WEEKDAYS.map((day, i) => (
+
+ {day}
+
+ ))}
+
+
+ {weeks.map((week, wi) => (
+
+ {week.map((day) => (
+
+ ))}
+
+ ))}
+
+
+
+ {format(selectedDate, 'EEEE, MMMM d')}
+ {selectedDayTasks.length > 0 && (
+
+ {selectedDayTasks.length} event{selectedDayTasks.length === 1 ? '' : 's'}
+
+ )}
+
+
+ {selectedDayTasks.length === 0 ? (
+
+ No events on this day
+ Tap a date or use the bar below
+
+ ) : (
+ selectedDayTasks.map((task) => (
+
+
+ handleToggleComplete(task.id)}
+ activeOpacity={0.7}
+ >
+
+
+
+ router.push({ pathname: '/task-detail', params: { id: task.id } })}
+ activeOpacity={0.7}
+ >
+
+ {task.title}
+
+
+
+ openTaskMenu(task)} activeOpacity={0.7}>
+
+
+
+
+ {(subtasksMap[task.id] ?? []).length > 0 && (
+
+ {(subtasksMap[task.id] ?? []).map((sub) => (
+ router.push({ pathname: '/subtask-detail', params: { id: sub.id } })}
+ activeOpacity={0.7}
+ >
+
+
+ {sub.title}
+
+
+ ))}
+
+ )}
+
+ ))
+ )}
+
+
+
+
+ {modals(() => {})}
+
+ setMonthPickerVisible(false)}
/>
- ))}
-
-
-
-
-
-
- {format(visibleMonth, 'MMM yyyy').toUpperCase()}
-
-
-
-
-
- item.id}
- renderItem={renderTask}
- ItemSeparatorComponent={MemoSeparator}
- ListEmptyComponent={
- loading ? (
-
- Loading...
-
- ) : (
-
- No tasks scheduled.
- Use the bar below to add one
-
- )
- }
- contentContainerStyle={styles.listContent}
- />
-
-
-
- {modals(() => {})}
+ setYearPickerVisible(false)}
+ />
+
+
);
}
-interface DayButtonProps {
- day: Date;
- selected: boolean;
- current: boolean;
- onPress: (day: Date) => void;
- theme: ThemeColors;
+function byDayOf(byDay: Record, day: Date): Task[] {
+ return byDay[day.getDate()] ?? [];
}
-const DayButton = React.memo(function DayButton({ day, selected, current, onPress, theme }: DayButtonProps) {
+function cellTitle(tasks: Task[]): string | null {
+ if (tasks.length === 0) return null;
+ if (tasks.length === 1) return tasks[0].title;
+ return `${tasks[0].title} +${tasks.length - 1}`;
+}
+
+function cellColor(tasks: Task[], categories: { id: string; color: string }[]): string {
+ if (tasks.length === 0) return '#8E8E8E';
+ const cat = categories.find((c) => c.id === tasks[0].categoryId);
+ return cat?.color ?? '#8E8E8E';
+}
+
+interface DayCellProps {
+ day: Date;
+ label: string | null;
+ dotColor: string;
+ selected: boolean;
+ today: boolean;
+ inMonth: boolean;
+ theme: ThemeColors;
+ onPress: (day: Date) => void;
+}
+
+const DayCell = React.memo(function DayCell({ day, label, dotColor, selected, today, inMonth, theme, onPress }: DayCellProps) {
return (
onPress(day)}
activeOpacity={0.7}
>
-
- {format(day, 'EEE').charAt(0)}
-
-
+
{format(day, 'd')}
+ {label ? (
+
+
+ {label}
+
+
+ ) : !inMonth ? (
+
+ ) : null}
);
});
-const MemoSeparator = React.memo(function Separator() {
- return ;
-});
-
const styles = StyleSheet.create({
container: {
flex: 1,
},
- dateStrip: {
- paddingHorizontal: 16,
- paddingTop: 12,
- gap: DAY_GAP,
+ flex: {
+ flex: 1,
},
- dayButton: {
- width: DAY_WIDTH,
- height: 60,
- borderRadius: 16,
- borderWidth: 1,
- alignItems: 'center',
- justifyContent: 'center',
- gap: 2,
+ scrollContent: {
+ paddingBottom: 96,
+ flexGrow: 1,
},
- dayTextSelected: {
- color: '#FFFFFF',
- },
- dayWeekday: {
- fontSize: 11,
- fontWeight: '600',
- textTransform: 'uppercase',
- },
- dayNumber: {
- fontSize: 16,
- fontWeight: '600',
+ calendarArea: {
+ paddingHorizontal: 12,
+ paddingTop: 6,
+ paddingBottom: 4,
},
monthRow: {
flexDirection: 'row',
alignItems: 'center',
- justifyContent: 'center',
- gap: 24,
- paddingVertical: 12,
+ justifyContent: 'space-between',
+ paddingHorizontal: 4,
+ paddingBottom: 14,
},
monthNav: {
width: 36,
@@ -228,34 +452,158 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'center',
},
- monthLabel: {
- fontSize: 15,
- fontWeight: '700',
- letterSpacing: 1,
- minWidth: 120,
- textAlign: 'center',
+ monthSelectorGroup: {
+ alignItems: 'center',
},
- listContent: {
- paddingHorizontal: 16,
- paddingTop: 4,
- paddingBottom: 100,
- flexGrow: 1,
- },
- separator: {
- height: 8,
- },
- emptyState: {
- flex: 1,
+ monthButton: {
+ flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
- paddingVertical: 64,
+ gap: 6,
+ paddingHorizontal: 10,
+ },
+ monthLabel: {
+ fontSize: 22,
+ fontWeight: '700',
+ },
+ yearButton: {
+ marginTop: -2,
+ },
+ yearLabel: {
+ fontSize: 13,
+ fontWeight: '500',
+ },
+ 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: '#FFFFFF',
+ },
+ 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: 16,
+ fontSize: 15,
fontWeight: '600',
marginBottom: 4,
},
emptySubtext: {
fontSize: 13,
},
-});
+ eventCard: {
+ marginHorizontal: 16,
+ marginTop: 6,
+ borderRadius: 16,
+ borderWidth: 1,
+ paddingHorizontal: 14,
+ paddingVertical: 12,
+ },
+ eventRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ checkCircle: {
+ width: 24,
+ marginRight: 10,
+ },
+ eventTitleTouch: {
+ flex: 1,
+ },
+ eventTitle: {
+ fontSize: 15,
+ fontWeight: '500',
+ },
+ eventCompleted: {
+ textDecorationLine: 'line-through',
+ color: '#9E9E9E',
+ },
+ menuButton: {
+ padding: 4,
+ marginLeft: 6,
+ },
+ bullets: {
+ marginTop: 8,
+ paddingTop: 8,
+ borderTopWidth: 1,
+ borderTopColor: 'rgba(255,255,255,0.06)',
+ gap: 6,
+ },
+ bulletRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ },
+ bulletDot: {
+ marginLeft: 8,
+ },
+ bulletText: {
+ flex: 1,
+ fontSize: 13,
+ },
+ bulletCompleted: {
+ textDecorationLine: 'line-through',
+ color: '#6E6E6E',
+ },
+});
\ No newline at end of file
diff --git a/carry-your-live/app/(tabs)/index.tsx b/carry-your-live/app/(tabs)/index.tsx
index bcf7377..d852f5a 100644
--- a/carry-your-live/app/(tabs)/index.tsx
+++ b/carry-your-live/app/(tabs)/index.tsx
@@ -22,7 +22,7 @@ export default function TasksScreen() {
return (
-
+
diff --git a/carry-your-live/app/(tabs)/settings.tsx b/carry-your-live/app/(tabs)/settings.tsx
index 7a2cd15..591b25d 100644
--- a/carry-your-live/app/(tabs)/settings.tsx
+++ b/carry-your-live/app/(tabs)/settings.tsx
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
-import { View, Text, StyleSheet, SafeAreaView, ScrollView, Switch, TouchableOpacity, Alert, Linking } from 'react-native';
+import { View, Text, StyleSheet, SafeAreaView, ScrollView, Switch, TouchableOpacity, Alert, Linking, Modal, Pressable } from 'react-native';
import { Header } from '@/components/Header';
import { ListItem } from '@/components/ListItem';
import { OptionPickerModal } from '@/components/OptionPickerModal';
@@ -9,16 +9,17 @@ 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 } from '@/theme';
+import { useSettings, SORT_OPTIONS, REMINDER_OPTIONS, ACCENT_PRESETS, DEFAULT_ACCENT } from '@/theme';
import { useCategories } from '@/hooks/useDatabase';
import { getAuthUser, getAuthToken } from '@/services/auth';
import { checkForUpdates, getCurrentAppVersion } from '@/services/updates';
import { getLastSyncTime } from '@/database/sync';
import Category from '@/models/Category';
import Svg, { Path } from 'react-native-svg';
+import ColorWheel from '@/components/ColorWheel';
export default function SettingsScreen() {
- const { theme, notifications, setNotifications, defaultCategoryId, setDefaultCategoryId, sortBy, setSortBy, reminderPreference, setReminderPreference, apiUrl } = useSettings();
+ const { theme, notifications, setNotifications, defaultCategoryId, setDefaultCategoryId, sortBy, setSortBy, reminderPreference, setReminderPreference, apiUrl, accentColor, setAccentColor } = useSettings();
const categories = useCategories();
const [picker, setPicker] = useState(null);
const [editingCategory, setEditingCategory] = useState(null);
@@ -26,6 +27,8 @@ export default function SettingsScreen() {
const [friendsVisible, setFriendsVisible] = useState(false);
const [legalVisible, setLegalVisible] = useState(null);
const [serverUrlVisible, setServerUrlVisible] = useState(false);
+ const [customAccentVisible, setCustomAccentVisible] = useState(false);
+ const [draftAccent, setDraftAccent] = useState(accentColor);
const [syncSubtitle, setSyncSubtitle] = useState('Checking...');
const [updateSubtitle, setUpdateSubtitle] = useState('Tap to check');
@@ -76,6 +79,11 @@ export default function SettingsScreen() {
const defaultCategoryLabel = defaultCategory?.name ?? (categories.length > 0 ? categories[0].name : 'None');
const reminderLabel = REMINDER_OPTIONS.find((o) => o.value === reminderPreference)?.label ?? 'No reminder';
+ const openCustomAccent = () => {
+ setDraftAccent(accentColor);
+ setCustomAccentVisible(true);
+ };
+
const handleDefaultCategory = (value: string | string[]) => {
setDefaultCategoryId(Array.isArray(value) ? value[0] : value);
};
@@ -148,6 +156,35 @@ export default function SettingsScreen() {
onPress={() => setPicker('sort')}
showChevron
/>
+ Appearance
+ Accent color
+
+ {ACCENT_PRESETS.map((c) => (
+ setAccentColor(c)}
+ activeOpacity={0.8}
+ >
+ {accentColor.toUpperCase() === c && (
+
+ )}
+
+ ))}
+
+ }
+ onPress={openCustomAccent}
+ showChevron
+ />
Data
setServerUrlVisible(false)} />
+ setCustomAccentVisible(false)}>
+ setCustomAccentVisible(false)}
+ >
+ {}}>
+
+ Custom Accent Color
+ setCustomAccentVisible(false)} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
+
+
+
+
+
+ { setDraftAccent(DEFAULT_ACCENT); setAccentColor(DEFAULT_ACCENT); }}
+ activeOpacity={0.7}
+ >
+ Reset
+
+ { setAccentColor(draftAccent); setCustomAccentVisible(false); }}
+ activeOpacity={0.8}
+ >
+ Apply
+
+
+
+
+
+
(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';
+}
+
const styles = StyleSheet.create({
container: {
flex: 1,
@@ -294,4 +376,73 @@ const styles = StyleSheet.create({
fontSize: 15,
fontWeight: '600',
},
+ sectionHint: {
+ fontSize: 12,
+ marginLeft: 4,
+ marginBottom: 8,
+ },
+ accentPresets: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: 10,
+ paddingHorizontal: 4,
+ marginBottom: 4,
+ },
+ accentSwatch: {
+ width: 36,
+ height: 36,
+ borderRadius: 18,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ accentSwatchSelected: {
+ borderWidth: 2,
+ borderColor: '#FFFFFF',
+ shadowColor: '#000',
+ shadowOpacity: 0.3,
+ shadowRadius: 3,
+ elevation: 3,
+ },
+ accentModalOverlay: {
+ flex: 1,
+ backgroundColor: 'rgba(0,0,0,0.6)',
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: 24,
+ },
+ accentModalSheet: {
+ width: '100%',
+ maxWidth: 400,
+ borderRadius: 16,
+ padding: 20,
+ },
+ accentModalHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ marginBottom: 16,
+ },
+ accentModalTitle: {
+ fontSize: 18,
+ fontWeight: '700',
+ },
+ accentModalActions: {
+ flexDirection: 'row',
+ gap: 12,
+ marginTop: 16,
+ },
+ accentModalButton: {
+ flex: 1,
+ paddingVertical: 12,
+ borderRadius: 12,
+ borderWidth: 1,
+ alignItems: 'center',
+ },
+ accentModalButtonPrimary: {
+ borderWidth: 0,
+ },
+ accentModalButtonText: {
+ fontSize: 15,
+ fontWeight: '600',
+ },
});
diff --git a/carry-your-live/app/add-task.tsx b/carry-your-live/app/add-task.tsx
index 8df1e7b..13904b9 100644
--- a/carry-your-live/app/add-task.tsx
+++ b/carry-your-live/app/add-task.tsx
@@ -15,7 +15,7 @@ import { AssigneeSelector } from '@/components/AssigneeSelector';
import { useForm, FormProvider, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
-import { useDatabase, useCategories } from '@/hooks/useDatabase';
+import { useDatabase } from '@/hooks/useDatabase';
import { database, collections } from '@/database';
import { TaskFormData } from '@/types';
import { useSettings } from '@/theme';
@@ -25,7 +25,7 @@ import { useFriends } from '@/hooks/useFriends';
const taskSchema = z.object({
title: z.string().trim().min(1, 'Task name is required').max(100),
description: z.string().max(1000).optional(),
- categoryId: z.string().min(1, 'Category is required'),
+ categoryId: z.string().optional(),
priority: z.enum(['none', 'low', 'medium', 'high', 'critical']),
dueDate: z.date().nullable().optional(),
dueTime: z.string().optional(),
@@ -42,13 +42,12 @@ const taskSchema = z.object({
export default function AddTaskScreen() {
const { isReady } = useDatabase();
- const categories = useCategories();
const { defaultCategoryId } = useSettings();
const router = useRouter();
const { theme } = useSettings();
const { date: dateParam } = useLocalSearchParams<{ date?: string }>();
const { friends } = useFriends();
- const initialCategory = defaultCategoryId || categories[0]?.id || '';
+ const initialCategory = defaultCategoryId || '';
const initialDate = useMemo(() => {
if (!dateParam) return null;
const parsed = new Date(Array.isArray(dateParam) ? dateParam[0] : dateParam);
@@ -108,6 +107,7 @@ export default function AddTaskScreen() {
const seriesId = data.repeat !== 'none'
? `series_${Math.random().toString(36).slice(2, 10)}${Date.now().toString(36)}`
: '';
+ const resolvedCategoryId = data.categoryId || '';
let createdTask: any = null;
@@ -115,7 +115,7 @@ export default function AddTaskScreen() {
const task = await collections.tasks.create((t) => {
t.title = data.title.trim();
t.description = data.description || '';
- t.categoryId = data.categoryId;
+ t.categoryId = resolvedCategoryId;
t.priority = data.priority;
t.completed = false;
t.dueDate = dueDateTimestamp;
diff --git a/carry-your-live/app/task-detail.tsx b/carry-your-live/app/task-detail.tsx
index 6219fc6..4370c3f 100644
--- a/carry-your-live/app/task-detail.tsx
+++ b/carry-your-live/app/task-detail.tsx
@@ -29,7 +29,7 @@ import Svg, { Path, Circle } from 'react-native-svg';
const taskSchema = z.object({
title: z.string().trim().min(1, 'Task name is required').max(100),
description: z.string().max(1000).optional(),
- categoryId: z.string().min(1, 'Category is required'),
+ categoryId: z.string().optional(),
priority: z.enum(['none', 'low', 'medium', 'high', 'critical']),
dueDate: z.date().nullable().optional(),
dueTime: z.string().optional(),
@@ -167,7 +167,7 @@ export default function TaskDetailScreen() {
await task.update((t) => {
t.title = data.title.trim();
t.description = data.description || '';
- t.categoryId = data.categoryId;
+ t.categoryId = data.categoryId || task.categoryId || '';
t.priority = data.priority;
t.dueDate = dueDateTimestamp;
t.dueTime = data.dueTime || '';
diff --git a/carry-your-live/package-lock.json b/carry-your-live/package-lock.json
index e57b372..4b83cb3 100644
--- a/carry-your-live/package-lock.json
+++ b/carry-your-live/package-lock.json
@@ -28,13 +28,10 @@
"react-hook-form": "^7.51.5",
"react-native": "0.86.2",
"react-native-gesture-handler": "2.32.0",
- "react-native-paper": "^5.12.3",
- "react-native-reanimated": "4.5.1",
"react-native-safe-area-context": "5.7.0",
"react-native-screens": "4.26.0",
"react-native-svg": "^15.15.4",
"react-native-web": "^0.21.2",
- "react-native-worklets": "0.10.1",
"zod": "^3.23.8"
},
"devDependencies": {
@@ -577,6 +574,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz",
"integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7"
},
@@ -1058,6 +1056,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz",
"integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7"
},
@@ -1073,6 +1072,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz",
"integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7"
},
@@ -1191,28 +1191,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/@callstack/react-theme-provider": {
- "version": "3.0.9",
- "resolved": "https://registry.npmjs.org/@callstack/react-theme-provider/-/react-theme-provider-3.0.9.tgz",
- "integrity": "sha512-tTQ0uDSCL0ypeMa8T/E9wAZRGKWj8kXP7+6RYgPTfOPs9N07C9xM8P02GJ3feETap4Ux5S69D9nteq9mEj86NA==",
- "license": "MIT",
- "dependencies": {
- "deepmerge": "^3.2.0",
- "hoist-non-react-statics": "^3.3.0"
- },
- "peerDependencies": {
- "react": ">=16.3.0"
- }
- },
- "node_modules/@callstack/react-theme-provider/node_modules/deepmerge": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-3.3.0.tgz",
- "integrity": "sha512-GRQOafGHwMHpjPx9iCvTgpu9NojZ49q794EEL94JVEw6VaeA8XTUyBKvAkOOjBX9oJNiV6G3P+T+tihFjo2TqA==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/@egjs/hammerjs": {
"version": "2.0.17",
"resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
@@ -10285,61 +10263,18 @@
"resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.3.1.tgz",
"integrity": "sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA==",
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
- "node_modules/react-native-paper": {
- "version": "5.15.3",
- "resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.15.3.tgz",
- "integrity": "sha512-GEyNTmWElIZgnYw09AjjCNupRYzCmP79uAAyGSyCEUZz7KBz1wtJcC0wVUkozR1Rn3PK/td/9LlR6+F1hzmYvA==",
- "license": "MIT",
- "workspaces": [
- "example",
- "docs"
- ],
- "dependencies": {
- "@callstack/react-theme-provider": "^3.0.9",
- "color": "^3.1.2",
- "use-latest-callback": "^0.2.3"
- },
- "peerDependencies": {
- "react": "*",
- "react-native": "*",
- "react-native-safe-area-context": "*"
- }
- },
- "node_modules/react-native-paper/node_modules/color": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
- "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
- "license": "MIT",
- "dependencies": {
- "color-convert": "^1.9.3",
- "color-string": "^1.6.0"
- }
- },
- "node_modules/react-native-paper/node_modules/color-convert": {
- "version": "1.9.3",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
- "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
- "license": "MIT",
- "dependencies": {
- "color-name": "1.1.3"
- }
- },
- "node_modules/react-native-paper/node_modules/color-name": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
- "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
- "license": "MIT"
- },
"node_modules/react-native-reanimated": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.5.1.tgz",
"integrity": "sha512-RnMvtDuR+68ig864gAvZCOdZehqhC5rFmMo0kn+ARfgVSTvFeF6IFLBVgMPUu0KwihaapEyW24WRi6nEyy1kSA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"react-native-is-edge-to-edge": "^1.3.1",
"semver": "^7.7.3"
@@ -10426,6 +10361,7 @@
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.10.1.tgz",
"integrity": "sha512-62mRM19bDpfpdI8HLkEErcdOsrAPDtE9lA/sw+5lLRpzBHNhxaoj9QyY2KjXqUmirelxkX4zuPGTC3VdA0feJA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/plugin-transform-arrow-functions": "^7.27.1",
"@babel/plugin-transform-class-properties": "^7.28.6",
diff --git a/carry-your-live/src/components/CategoryEditorModal.tsx b/carry-your-live/src/components/CategoryEditorModal.tsx
index 214b83c..45d3dce 100644
--- a/carry-your-live/src/components/CategoryEditorModal.tsx
+++ b/carry-your-live/src/components/CategoryEditorModal.tsx
@@ -132,7 +132,7 @@ export function CategoryEditorModal({ visible, category, categoryCount, onClose
disabled={!name.trim()}
activeOpacity={0.8}
>
- Save
+ Save
@@ -231,6 +231,5 @@ const styles = StyleSheet.create({
saveButtonText: {
fontSize: 15,
fontWeight: '600',
- color: '#FFFFFF',
},
});
diff --git a/carry-your-live/src/components/CategorySelector.tsx b/carry-your-live/src/components/CategorySelector.tsx
index 2b9ae74..5ce84e7 100644
--- a/carry-your-live/src/components/CategorySelector.tsx
+++ b/carry-your-live/src/components/CategorySelector.tsx
@@ -28,10 +28,10 @@ export function CategorySelector({ value, onChange, error }: CategorySelectorPro
activeOpacity={0.8}
>
-
-
- {selectedCategory?.name || 'Select category'}
-
+
+
+ {selectedCategory?.name || 'None'}
+