fixxed big ui cavia
Build APK / build (push) Canceled after 0s

This commit is contained in:
2026-08-07 01:06:03 +02:00
parent 3d594670b0
commit ecb7fbefb1
71 changed files with 3989 additions and 1204 deletions
+9
View File
@@ -45,6 +45,15 @@ export default function TabLayout() {
),
}}
/>
<Tabs.Screen
name="stats"
options={{
title: 'Stats',
tabBarIcon: ({ focused, color }) => (
<TabBarIcon name="stats" focused={focused} color={color} />
),
}}
/>
<Tabs.Screen
name="settings"
options={{
+79 -49
View File
@@ -1,15 +1,17 @@
import React, { useMemo, useRef, useState } from 'react';
import React, { useMemo, useRef, useState, useCallback } from 'react';
import { View, Text, StyleSheet, SafeAreaView, ScrollView, TouchableOpacity, FlatList } from 'react-native';
import { useRouter } from 'expo-router';
import { Header } from '@/components/Header';
import { useTasksByDate } from '@/hooks/useTasks';
import { useTaskModals } from '@/hooks/useTaskModals';
import { toggleTaskComplete } from '@/utils/taskActions';
import { useSettings } from '@/theme';
import { TaskItem } from '@/components/TaskItem';
import { QuickAddBar } from '@/components/QuickAddBar';
import { TaskData } from '@/types';
import { format, startOfMonth, endOfMonth, eachDayOfInterval, isSameDay, isSameMonth, addMonths, isToday } from 'date-fns';
import { format, startOfMonth, endOfMonth, eachDayOfInterval, isSameDay, isSameMonth, addMonths, isToday, startOfDay } from 'date-fns';
import Svg, { Path } from 'react-native-svg';
import type { ThemeColors } from '@/theme';
const DAY_WIDTH = 44;
const DAY_GAP = 6;
@@ -17,6 +19,7 @@ const DAY_GAP = 6;
export default function CalendarScreen() {
const router = useRouter();
const { theme } = useSettings();
const { modals, openTaskMenu, openTaskDelete } = useTaskModals();
const [visibleMonth, setVisibleMonth] = useState(() => new Date());
const [selectedDate, setSelectedDate] = useState(() => new Date());
const stripRef = useRef<ScrollView>(null);
@@ -28,46 +31,57 @@ export default function CalendarScreen() {
const { tasks, loading } = useTasksByDate(selectedDate);
const handleDayPress = (day: Date) => {
const handleDayPress = useCallback((day: Date) => {
setSelectedDate(day);
if (!isSameMonth(day, visibleMonth)) {
setVisibleMonth(day);
}
};
}, [visibleMonth]);
const handlePrevMonth = () => {
const handlePrevMonth = useCallback(() => {
const prev = addMonths(visibleMonth, -1);
setVisibleMonth(prev);
if (!isSameMonth(selectedDate, prev)) {
setSelectedDate(startOfMonth(prev));
}
};
}, [visibleMonth, selectedDate]);
const handleNextMonth = () => {
const handleNextMonth = useCallback(() => {
const next = addMonths(visibleMonth, 1);
setVisibleMonth(next);
if (!isSameMonth(selectedDate, next)) {
setSelectedDate(startOfMonth(next));
}
};
}, [visibleMonth, selectedDate]);
const handleToggleComplete = async (taskId: string) => {
const handleToggleComplete = useCallback(async (taskId: string) => {
await toggleTaskComplete(taskId);
};
}, []);
const today = new Date();
const scrollToDay = (day: Date) => {
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(() => {
scrollToDay(isSameMonth(selectedDate, visibleMonth) ? selectedDate : today);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [visibleMonth]);
const target = isSameMonth(selectedDate, visibleMonth) ? selectedDate : startOfDay(new Date());
scrollToDay(target);
}, [visibleMonth, scrollToDay, selectedDate]);
const renderTask = useCallback(
({ item }: { item: TaskData }) => (
<TaskItem
task={item}
onToggle={() => handleToggleComplete(item.id)}
onDelete={() => openTaskDelete(item)}
onPress={() => router.push({ pathname: '/task-detail', params: { id: item.id } })}
onMenuOpen={() => openTaskMenu(item)}
/>
),
[handleToggleComplete, openTaskDelete, openTaskMenu, router]
);
return (
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
@@ -79,30 +93,16 @@ export default function CalendarScreen() {
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.dateStrip}
>
{days.map((day) => {
const isSelected = isSameDay(day, selectedDate);
const isCurrent = isToday(day);
return (
<TouchableOpacity
key={day.toISOString()}
style={[
styles.dayButton,
{ backgroundColor: theme.card, borderColor: theme.borderStrong },
isCurrent && !isSelected && { borderColor: theme.accent, borderWidth: 1.5 },
isSelected && { backgroundColor: theme.accent, borderColor: theme.accent },
]}
onPress={() => handleDayPress(day)}
activeOpacity={0.7}
>
<Text style={[styles.dayWeekday, { color: theme.textMuted }, isSelected && styles.dayTextSelected]}>
{format(day, 'EEE').charAt(0)}
</Text>
<Text style={[styles.dayNumber, { color: theme.text }, isSelected && styles.dayTextSelected]}>
{format(day, 'd')}
</Text>
</TouchableOpacity>
);
})}
{days.map((day) => (
<DayButton
key={day.toISOString()}
day={day}
selected={isSameDay(day, selectedDate)}
current={isToday(day) && !isSameDay(day, selectedDate)}
onPress={handleDayPress}
theme={theme}
/>
))}
</ScrollView>
<View style={styles.monthRow}>
@@ -122,14 +122,8 @@ export default function CalendarScreen() {
<FlatList
data={tasks}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<TaskItem
task={item as TaskData}
onToggle={() => handleToggleComplete(item.id)}
onPress={() => router.push({ pathname: '/task-detail', params: { id: item.id } })}
/>
)}
ItemSeparatorComponent={() => <View style={styles.separator} />}
renderItem={renderTask}
ItemSeparatorComponent={MemoSeparator}
ListEmptyComponent={
loading ? (
<View style={styles.emptyState}>
@@ -149,10 +143,46 @@ export default function CalendarScreen() {
dueDate={selectedDate.getTime()}
placeholder={`Add task for ${format(selectedDate, 'MMM d')}`}
/>
{modals(() => {})}
</SafeAreaView>
);
}
interface DayButtonProps {
day: Date;
selected: boolean;
current: boolean;
onPress: (day: Date) => void;
theme: ThemeColors;
}
const DayButton = React.memo(function DayButton({ day, selected, current, onPress, theme }: DayButtonProps) {
return (
<TouchableOpacity
style={[
styles.dayButton,
{ backgroundColor: theme.card, borderColor: theme.borderStrong },
current && { borderColor: theme.accent, borderWidth: 1.5 },
selected && { backgroundColor: theme.accent, borderColor: theme.accent },
]}
onPress={() => onPress(day)}
activeOpacity={0.7}
>
<Text style={[styles.dayWeekday, { color: theme.textMuted }, selected && styles.dayTextSelected]}>
{format(day, 'EEE').charAt(0)}
</Text>
<Text style={[styles.dayNumber, { color: theme.text }, selected && styles.dayTextSelected]}>
{format(day, 'd')}
</Text>
</TouchableOpacity>
);
});
const MemoSeparator = React.memo(function Separator() {
return <View style={styles.separator} />;
});
const styles = StyleSheet.create({
container: {
flex: 1,
+7 -3
View File
@@ -11,7 +11,6 @@ export default function TasksScreen() {
const { isReady } = useDatabase();
const { theme } = useSettings();
const [selectedCategory, setSelectedCategory] = React.useState<string>('all');
const [selectionActive, setSelectionActive] = React.useState(false);
if (!isReady) {
return (
@@ -24,8 +23,10 @@ export default function TasksScreen() {
return (
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
<Header title="TODO" showLogo={true} />
<CategoryFilter selected={selectedCategory} onSelect={setSelectedCategory} />
<TaskList categoryId={selectedCategory} onSelectionChange={setSelectionActive} />
<View style={styles.categoryFilterWrapper}>
<CategoryFilter selected={selectedCategory} onSelect={setSelectedCategory} />
</View>
<TaskList categoryId={selectedCategory} />
<QuickAddBar />
</SafeAreaView>
);
@@ -35,6 +36,9 @@ const styles = StyleSheet.create({
container: {
flex: 1,
},
categoryFilterWrapper: {
height: 36,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
+36 -6
View File
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { View, Text, StyleSheet, SafeAreaView, ScrollView, Switch, TouchableOpacity } from 'react-native';
import { View, Text, StyleSheet, SafeAreaView, ScrollView, Switch, TouchableOpacity, Alert, Linking } from 'react-native';
import { Header } from '@/components/Header';
import { ListItem } from '@/components/ListItem';
import { OptionPickerModal } from '@/components/OptionPickerModal';
@@ -12,6 +12,7 @@ import SyncStatus from '@/components/SyncStatus';
import { useSettings, SORT_OPTIONS, REMINDER_OPTIONS } from '@/theme';
import { useCategories } from '@/hooks/useDatabase';
import { getAuthUser, getAuthToken } from '@/services/auth';
import { checkForUpdates, getCurrentAppVersion } from '@/services/updates';
import { getLastSyncTime } from '@/database/sync';
import Category from '@/models/Category';
import Svg, { Path } from 'react-native-svg';
@@ -26,6 +27,28 @@ export default function SettingsScreen() {
const [legalVisible, setLegalVisible] = useState<null | 'privacy' | 'terms'>(null);
const [serverUrlVisible, setServerUrlVisible] = useState(false);
const [syncSubtitle, setSyncSubtitle] = useState('Checking...');
const [updateSubtitle, setUpdateSubtitle] = useState('Tap to check');
const handleCheckUpdates = async () => {
setUpdateSubtitle('Checking...');
const update = await checkForUpdates();
if (!update) {
setUpdateSubtitle('Up to date');
Alert.alert('Up to date', `You're running the latest version (${getCurrentAppVersion()}).`);
return;
}
setUpdateSubtitle(`${update.version} available`);
const url = update.apkUrl ?? update.releaseUrl;
Alert.alert('Update available', `Version ${update.version} is available for download.`, [
{ text: 'Later', style: 'cancel' },
{
text: 'Download',
onPress: () => {
if (url) Linking.openURL(url).catch(() => {});
},
},
]);
};
const refreshSyncStatus = async () => {
const token = await getAuthToken();
@@ -45,6 +68,7 @@ export default function SettingsScreen() {
};
React.useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
refreshSyncStatus();
}, []);
@@ -52,12 +76,12 @@ 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 handleDefaultCategory = (value: string) => {
setDefaultCategoryId(value);
const handleDefaultCategory = (value: string | string[]) => {
setDefaultCategoryId(Array.isArray(value) ? value[0] : value);
};
const handleReminderPreference = (value: string) => {
setReminderPreference(value as typeof reminderPreference);
const handleReminderPreference = (value: string | string[]) => {
setReminderPreference((Array.isArray(value) ? value[0] : value) as typeof reminderPreference);
};
const editorVisible = editingCategory !== null;
@@ -146,9 +170,15 @@ export default function SettingsScreen() {
showChevron
/>
<Text style={[styles.sectionTitle, { color: theme.textMuted }]}>About</Text>
<ListItem
title="Check for Updates"
subtitle={updateSubtitle}
onPress={handleCheckUpdates}
showChevron
/>
<ListItem
title="Version"
subtitle="1.0.0"
subtitle={getCurrentAppVersion()}
/>
<ListItem
title="Privacy Policy"
+201
View File
@@ -0,0 +1,201 @@
import React from 'react';
import { View, Text, StyleSheet, SafeAreaView, ScrollView } from 'react-native';
import { Header } from '@/components/Header';
import { useStats } from '@/hooks/useStats';
import { useSettings } from '@/theme';
export default function StatsScreen() {
const { theme } = useSettings();
const stats = useStats();
const maxDaily = Math.max(1, ...stats.daily.map((d) => d.count));
return (
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
<Header title="Stats" showLogo={true} />
<ScrollView contentContainerStyle={styles.content} showsVerticalScrollIndicator={false}>
<View style={[styles.card, { backgroundColor: theme.card }]}>
<Text style={[styles.cardTitle, { color: theme.textMuted }]}>Overview</Text>
<View style={styles.summaryGrid}>
<View style={styles.summaryItem}>
<Text style={[styles.summaryValue, { color: theme.accent }]}>{stats.totalCompleted}</Text>
<Text style={[styles.summaryLabel, { color: theme.textMuted }]}>Completed</Text>
</View>
<View style={styles.summaryItem}>
<Text style={[styles.summaryValue, { color: theme.text }]}>{stats.completedLast7}</Text>
<Text style={[styles.summaryLabel, { color: theme.textMuted }]}>Last 7 days</Text>
</View>
<View style={styles.summaryItem}>
<Text style={[styles.summaryValue, { color: theme.text }]}>{stats.completedLast30}</Text>
<Text style={[styles.summaryLabel, { color: theme.textMuted }]}>Last 30 days</Text>
</View>
<View style={styles.summaryItem}>
<Text style={[styles.summaryValue, { color: theme.text }]}>{stats.completionRate}%</Text>
<Text style={[styles.summaryLabel, { color: theme.textMuted }]}>Success rate</Text>
</View>
<View style={styles.summaryItem}>
<Text style={[styles.summaryValue, { color: theme.text }]}>{stats.currentStreak}</Text>
<Text style={[styles.summaryLabel, { color: theme.textMuted }]}>Day streak</Text>
</View>
<View style={styles.summaryItem}>
<Text style={[styles.summaryValue, { color: stats.overdueCount > 0 ? theme.accent : theme.text }]}>
{stats.overdueCount}
</Text>
<Text style={[styles.summaryLabel, { color: theme.textMuted }]}>Overdue</Text>
</View>
</View>
</View>
<View style={[styles.card, { backgroundColor: theme.card }]}>
<Text style={[styles.cardTitle, { color: theme.textMuted }]}>Last 7 days</Text>
<View style={styles.chartRow}>
{stats.daily.map((d, i) => {
const height = (d.count / maxDaily) * BASE_HEIGHT;
return (
<View key={i} style={styles.chartCol}>
<View style={styles.chartBarTrack}>
<View
style={[
styles.chartBar,
{ height: Math.max(4, height), backgroundColor: d.count > 0 ? theme.accent : theme.cardAlt },
]}
/>
</View>
<Text style={[styles.chartLabel, { color: theme.textFaint }]}>{d.label}</Text>
</View>
);
})}
</View>
</View>
<View style={[styles.card, { backgroundColor: theme.card }]}>
<Text style={[styles.cardTitle, { color: theme.textMuted }]}>By category</Text>
{stats.byCategory.length === 0 ? (
<Text style={[styles.emptyText, { color: theme.textFaint }]}>No completed tasks yet</Text>
) : (
stats.byCategory.map((c) => (
<View key={c.id} style={styles.row}>
<View style={[styles.dot, { backgroundColor: c.color }]} />
<Text style={[styles.rowLabel, { color: theme.text }]}>{c.name}</Text>
<Text style={[styles.rowValue, { color: theme.textMuted }]}>{c.count}</Text>
</View>
))
)}
</View>
<View style={[styles.card, { backgroundColor: theme.card }]}>
<Text style={[styles.cardTitle, { color: theme.textMuted }]}>By priority</Text>
{stats.byPriority.length === 0 ? (
<Text style={[styles.emptyText, { color: theme.textFaint }]}>No completed tasks yet</Text>
) : (
stats.byPriority.map((p) => (
<View key={p.priority} style={styles.row}>
<View style={[styles.dot, { backgroundColor: priorityColor(p.priority) }]} />
<Text style={[styles.rowLabel, { color: theme.text }]}>{p.label}</Text>
<Text style={[styles.rowValue, { color: theme.textMuted }]}>{p.count}</Text>
</View>
))
)}
</View>
</ScrollView>
</SafeAreaView>
);
}
const BASE_HEIGHT = 90;
function priorityColor(priority: string): string {
switch (priority) {
case 'high':
return '#EF5350';
case 'medium':
return '#FFA726';
case 'low':
return '#66BB6A';
default:
return '#9E9E9E';
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
content: {
padding: 16,
paddingBottom: 32,
gap: 12,
},
card: {
borderRadius: 16,
padding: 16,
},
cardTitle: {
fontSize: 13,
fontWeight: '600',
textTransform: 'uppercase',
letterSpacing: 0.5,
marginBottom: 14,
},
summaryGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
},
summaryItem: {
width: '33.33%',
marginBottom: 16,
},
summaryValue: {
fontSize: 26,
fontWeight: '700',
},
summaryLabel: {
fontSize: 12,
marginTop: 2,
},
chartRow: {
flexDirection: 'row',
alignItems: 'flex-end',
gap: 8,
},
chartCol: {
flex: 1,
alignItems: 'center',
},
chartBarTrack: {
height: BASE_HEIGHT,
justifyContent: 'flex-end',
width: '100%',
},
chartBar: {
width: '100%',
borderRadius: 6,
minHeight: 4,
},
chartLabel: {
fontSize: 11,
marginTop: 6,
},
row: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 8,
},
dot: {
width: 10,
height: 10,
borderRadius: 5,
marginRight: 10,
},
rowLabel: {
flex: 1,
fontSize: 15,
},
rowValue: {
fontSize: 15,
fontWeight: '600',
},
emptyText: {
fontSize: 14,
},
});