This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import { View, Text, StyleSheet, SafeAreaView, ScrollView, TouchableOpacity, FlatList } from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { Header } from '@/components/Header';
|
||||
import { useTasksByDate } from '@/hooks/useTasks';
|
||||
import { toggleTaskComplete } from '@/utils/taskActions';
|
||||
import { useSettings } from '@/theme';
|
||||
import { TaskItem } from '@/components/TaskItem';
|
||||
import { QuickAddBar } from '@/components/QuickAddBar';
|
||||
import { TaskData } from '@/types';
|
||||
import { format, startOfMonth, endOfMonth, eachDayOfInterval, isSameDay, isSameMonth, addMonths, isToday } from 'date-fns';
|
||||
import Svg, { Path } from 'react-native-svg';
|
||||
|
||||
const DAY_WIDTH = 44;
|
||||
const DAY_GAP = 6;
|
||||
|
||||
export default function CalendarScreen() {
|
||||
const router = useRouter();
|
||||
const { theme } = useSettings();
|
||||
const [visibleMonth, setVisibleMonth] = useState(() => new Date());
|
||||
const [selectedDate, setSelectedDate] = useState(() => new Date());
|
||||
const stripRef = useRef<ScrollView>(null);
|
||||
|
||||
const days = useMemo(
|
||||
() => eachDayOfInterval({ start: startOfMonth(visibleMonth), end: endOfMonth(visibleMonth) }),
|
||||
[visibleMonth]
|
||||
);
|
||||
|
||||
const { tasks, loading } = useTasksByDate(selectedDate);
|
||||
|
||||
const handleDayPress = (day: Date) => {
|
||||
setSelectedDate(day);
|
||||
if (!isSameMonth(day, visibleMonth)) {
|
||||
setVisibleMonth(day);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrevMonth = () => {
|
||||
const prev = addMonths(visibleMonth, -1);
|
||||
setVisibleMonth(prev);
|
||||
if (!isSameMonth(selectedDate, prev)) {
|
||||
setSelectedDate(startOfMonth(prev));
|
||||
}
|
||||
};
|
||||
|
||||
const handleNextMonth = () => {
|
||||
const next = addMonths(visibleMonth, 1);
|
||||
setVisibleMonth(next);
|
||||
if (!isSameMonth(selectedDate, next)) {
|
||||
setSelectedDate(startOfMonth(next));
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleComplete = async (taskId: string) => {
|
||||
await toggleTaskComplete(taskId);
|
||||
};
|
||||
|
||||
const today = new Date();
|
||||
|
||||
const scrollToDay = (day: Date) => {
|
||||
const index = days.findIndex((d) => isSameDay(d, day));
|
||||
if (index >= 0) {
|
||||
stripRef.current?.scrollTo({ x: Math.max(0, index * (DAY_WIDTH + DAY_GAP) - 24), animated: true });
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
scrollToDay(isSameMonth(selectedDate, visibleMonth) ? selectedDate : today);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [visibleMonth]);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
||||
<Header title="Calendar" showLogo={false} />
|
||||
|
||||
<ScrollView
|
||||
ref={stripRef}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.dateStrip}
|
||||
>
|
||||
{days.map((day) => {
|
||||
const isSelected = isSameDay(day, selectedDate);
|
||||
const isCurrent = isToday(day);
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={day.toISOString()}
|
||||
style={[
|
||||
styles.dayButton,
|
||||
{ backgroundColor: theme.card, borderColor: theme.borderStrong },
|
||||
isCurrent && !isSelected && { borderColor: theme.accent, borderWidth: 1.5 },
|
||||
isSelected && { backgroundColor: theme.accent, borderColor: theme.accent },
|
||||
]}
|
||||
onPress={() => handleDayPress(day)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[styles.dayWeekday, { color: theme.textMuted }, isSelected && styles.dayTextSelected]}>
|
||||
{format(day, 'EEE').charAt(0)}
|
||||
</Text>
|
||||
<Text style={[styles.dayNumber, { color: theme.text }, isSelected && styles.dayTextSelected]}>
|
||||
{format(day, 'd')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
|
||||
<View style={styles.monthRow}>
|
||||
<TouchableOpacity onPress={handlePrevMonth} style={[styles.monthNav, { borderColor: theme.borderStrong, backgroundColor: theme.card }]} activeOpacity={0.7}>
|
||||
<Svg width={20} height={20} viewBox="0 0 24 24">
|
||||
<Path d="M15 18l-6-6 6-6" stroke={theme.textFaint} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
</Svg>
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.monthLabel, { color: theme.text }]}>{format(visibleMonth, 'MMM yyyy').toUpperCase()}</Text>
|
||||
<TouchableOpacity onPress={handleNextMonth} style={[styles.monthNav, { borderColor: theme.borderStrong, backgroundColor: theme.card }]} activeOpacity={0.7}>
|
||||
<Svg width={20} height={20} viewBox="0 0 24 24">
|
||||
<Path d="M9 6l6 6-6 6" stroke={theme.textFaint} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
</Svg>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<FlatList
|
||||
data={tasks}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<TaskItem
|
||||
task={item as TaskData}
|
||||
onToggle={() => handleToggleComplete(item.id)}
|
||||
onPress={() => router.push({ pathname: '/task-detail', params: { id: item.id } })}
|
||||
/>
|
||||
)}
|
||||
ItemSeparatorComponent={() => <View style={styles.separator} />}
|
||||
ListEmptyComponent={
|
||||
loading ? (
|
||||
<View style={styles.emptyState}>
|
||||
<Text style={[styles.emptyText, { color: theme.textFaint }]}>Loading...</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.emptyState}>
|
||||
<Text style={[styles.emptyText, { color: theme.textSecondary }]}>No tasks scheduled.</Text>
|
||||
<Text style={[styles.emptySubtext, { color: theme.textMuted }]}>Use the bar below to add one</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
contentContainerStyle={styles.listContent}
|
||||
/>
|
||||
|
||||
<QuickAddBar
|
||||
dueDate={selectedDate.getTime()}
|
||||
placeholder={`Add task for ${format(selectedDate, 'MMM d')}`}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
dateStrip: {
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 12,
|
||||
gap: DAY_GAP,
|
||||
},
|
||||
dayButton: {
|
||||
width: DAY_WIDTH,
|
||||
height: 60,
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 2,
|
||||
},
|
||||
dayTextSelected: {
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
dayWeekday: {
|
||||
fontSize: 11,
|
||||
fontWeight: '600',
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
dayNumber: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
monthRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 24,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
monthNav: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
monthLabel: {
|
||||
fontSize: 15,
|
||||
fontWeight: '700',
|
||||
letterSpacing: 1,
|
||||
minWidth: 120,
|
||||
textAlign: 'center',
|
||||
},
|
||||
listContent: {
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 4,
|
||||
paddingBottom: 100,
|
||||
flexGrow: 1,
|
||||
},
|
||||
separator: {
|
||||
height: 8,
|
||||
},
|
||||
emptyState: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 64,
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
marginBottom: 4,
|
||||
},
|
||||
emptySubtext: {
|
||||
fontSize: 13,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user