- Expo SDK 54, expo-router, NativeWind - 7 screens: login, tasks, calendar, goals, chat, settings, tabs - API client (api.hasdo.info), SecureStore auth, AuthContext - Tab navigation: Ukoly, Kalendar, Cile, Chat, Nastaveni - Pull-to-refresh, FAB, group colors, priority dots - Calendar grid with task dots - AI chat with keyboard avoiding - Web export verified (780 modules, 1.5MB bundle) - Bundle ID: info.hasdo.taskteam Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
265 lines
7.2 KiB
TypeScript
265 lines
7.2 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
ScrollView,
|
|
RefreshControl,
|
|
TouchableOpacity,
|
|
Alert,
|
|
} from 'react-native';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { useAuthContext } from '../../lib/AuthContext';
|
|
import * as api from '../../lib/api';
|
|
|
|
const DAYS = ['Po', 'Ut', 'St', 'Ct', 'Pa', 'So', 'Ne'];
|
|
const MONTHS = [
|
|
'Leden', 'Unor', 'Brezen', 'Duben', 'Kveten', 'Cerven',
|
|
'Cervenec', 'Srpen', 'Zari', 'Rijen', 'Listopad', 'Prosinec',
|
|
];
|
|
|
|
function getDaysInMonth(year: number, month: number) {
|
|
return new Date(year, month + 1, 0).getDate();
|
|
}
|
|
|
|
function getFirstDayOfMonth(year: number, month: number) {
|
|
const day = new Date(year, month, 1).getDay();
|
|
return day === 0 ? 6 : day - 1; // Monday = 0
|
|
}
|
|
|
|
export default function CalendarScreen() {
|
|
const { token } = useAuthContext();
|
|
const [tasks, setTasks] = useState<any[]>([]);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const [currentDate, setCurrentDate] = useState(new Date());
|
|
const [selectedDate, setSelectedDate] = useState<string | null>(null);
|
|
|
|
const year = currentDate.getFullYear();
|
|
const month = currentDate.getMonth();
|
|
|
|
const loadTasks = useCallback(async () => {
|
|
if (!token) return;
|
|
try {
|
|
const res = await api.getTasks(token);
|
|
setTasks(res.data || []);
|
|
} catch (err: any) {
|
|
Alert.alert('Chyba', err.message);
|
|
}
|
|
}, [token]);
|
|
|
|
useEffect(() => {
|
|
loadTasks();
|
|
}, [loadTasks]);
|
|
|
|
const onRefresh = useCallback(async () => {
|
|
setRefreshing(true);
|
|
await loadTasks();
|
|
setRefreshing(false);
|
|
}, [loadTasks]);
|
|
|
|
const prevMonth = () => {
|
|
setCurrentDate(new Date(year, month - 1, 1));
|
|
setSelectedDate(null);
|
|
};
|
|
|
|
const nextMonth = () => {
|
|
setCurrentDate(new Date(year, month + 1, 1));
|
|
setSelectedDate(null);
|
|
};
|
|
|
|
const daysInMonth = getDaysInMonth(year, month);
|
|
const firstDay = getFirstDayOfMonth(year, month);
|
|
|
|
const tasksByDate: Record<string, any[]> = {};
|
|
tasks.forEach((t) => {
|
|
if (t.due_date) {
|
|
const d = t.due_date.substring(0, 10);
|
|
if (!tasksByDate[d]) tasksByDate[d] = [];
|
|
tasksByDate[d].push(t);
|
|
}
|
|
});
|
|
|
|
const today = new Date().toISOString().substring(0, 10);
|
|
|
|
const selectedTasks = selectedDate ? tasksByDate[selectedDate] || [] : [];
|
|
|
|
return (
|
|
<ScrollView
|
|
style={styles.container}
|
|
refreshControl={
|
|
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
|
}
|
|
>
|
|
{/* Month navigator */}
|
|
<View style={styles.monthNav}>
|
|
<TouchableOpacity onPress={prevMonth} style={styles.navBtn}>
|
|
<Ionicons name="chevron-back" size={24} color="#3B82F6" />
|
|
</TouchableOpacity>
|
|
<Text style={styles.monthTitle}>
|
|
{MONTHS[month]} {year}
|
|
</Text>
|
|
<TouchableOpacity onPress={nextMonth} style={styles.navBtn}>
|
|
<Ionicons name="chevron-forward" size={24} color="#3B82F6" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Day headers */}
|
|
<View style={styles.dayHeaders}>
|
|
{DAYS.map((d) => (
|
|
<Text key={d} style={styles.dayHeader}>
|
|
{d}
|
|
</Text>
|
|
))}
|
|
</View>
|
|
|
|
{/* Calendar grid */}
|
|
<View style={styles.grid}>
|
|
{Array.from({ length: firstDay }).map((_, i) => (
|
|
<View key={`empty-${i}`} style={styles.dayCell} />
|
|
))}
|
|
{Array.from({ length: daysInMonth }).map((_, i) => {
|
|
const day = i + 1;
|
|
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
const hasTasks = !!tasksByDate[dateStr];
|
|
const isToday = dateStr === today;
|
|
const isSelected = dateStr === selectedDate;
|
|
|
|
return (
|
|
<TouchableOpacity
|
|
key={day}
|
|
style={[
|
|
styles.dayCell,
|
|
isToday && styles.todayCell,
|
|
isSelected && styles.selectedCell,
|
|
]}
|
|
onPress={() => setSelectedDate(dateStr)}
|
|
>
|
|
<Text
|
|
style={[
|
|
styles.dayText,
|
|
isToday && styles.todayText,
|
|
isSelected && styles.selectedText,
|
|
]}
|
|
>
|
|
{day}
|
|
</Text>
|
|
{hasTasks && (
|
|
<View
|
|
style={[
|
|
styles.dot,
|
|
isSelected && { backgroundColor: '#fff' },
|
|
]}
|
|
/>
|
|
)}
|
|
</TouchableOpacity>
|
|
);
|
|
})}
|
|
</View>
|
|
|
|
{/* Selected date tasks */}
|
|
{selectedDate && (
|
|
<View style={styles.tasksSection}>
|
|
<Text style={styles.tasksSectionTitle}>
|
|
Ukoly na {selectedDate}
|
|
</Text>
|
|
{selectedTasks.length === 0 ? (
|
|
<Text style={styles.noTasks}>Zadne ukoly</Text>
|
|
) : (
|
|
selectedTasks.map((t) => (
|
|
<View key={t.id} style={styles.taskItem}>
|
|
<Ionicons
|
|
name={t.status === 'done' ? 'checkmark-circle' : 'ellipse-outline'}
|
|
size={20}
|
|
color={t.status === 'done' ? '#22C55E' : '#94A3B8'}
|
|
/>
|
|
<Text
|
|
style={[
|
|
styles.taskItemText,
|
|
t.status === 'done' && { textDecorationLine: 'line-through', color: '#94A3B8' },
|
|
]}
|
|
>
|
|
{t.title}
|
|
</Text>
|
|
</View>
|
|
))
|
|
)}
|
|
</View>
|
|
)}
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: '#F8FAFC' },
|
|
monthNav: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
padding: 16,
|
|
backgroundColor: '#fff',
|
|
},
|
|
navBtn: { padding: 8 },
|
|
monthTitle: { fontSize: 18, fontWeight: '700', color: '#1E293B' },
|
|
dayHeaders: {
|
|
flexDirection: 'row',
|
|
backgroundColor: '#fff',
|
|
paddingBottom: 8,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: '#E2E8F0',
|
|
},
|
|
dayHeader: {
|
|
flex: 1,
|
|
textAlign: 'center',
|
|
fontSize: 12,
|
|
fontWeight: '600',
|
|
color: '#64748B',
|
|
},
|
|
grid: {
|
|
flexDirection: 'row',
|
|
flexWrap: 'wrap',
|
|
backgroundColor: '#fff',
|
|
padding: 4,
|
|
},
|
|
dayCell: {
|
|
width: '14.28%',
|
|
aspectRatio: 1,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
todayCell: {
|
|
backgroundColor: '#EFF6FF',
|
|
borderRadius: 20,
|
|
},
|
|
selectedCell: {
|
|
backgroundColor: '#3B82F6',
|
|
borderRadius: 20,
|
|
},
|
|
dayText: { fontSize: 15, color: '#1E293B' },
|
|
todayText: { color: '#3B82F6', fontWeight: '700' },
|
|
selectedText: { color: '#fff', fontWeight: '700' },
|
|
dot: {
|
|
width: 5,
|
|
height: 5,
|
|
borderRadius: 3,
|
|
backgroundColor: '#3B82F6',
|
|
marginTop: 2,
|
|
},
|
|
tasksSection: {
|
|
margin: 12,
|
|
padding: 16,
|
|
backgroundColor: '#fff',
|
|
borderRadius: 12,
|
|
},
|
|
tasksSectionTitle: { fontSize: 16, fontWeight: '600', color: '#1E293B', marginBottom: 12 },
|
|
noTasks: { color: '#94A3B8', fontSize: 14 },
|
|
taskItem: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 10,
|
|
paddingVertical: 8,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: '#F1F5F9',
|
|
},
|
|
taskItemText: { fontSize: 14, color: '#1E293B', flex: 1 },
|
|
});
|