import React, { useState, useEffect, useCallback } from 'react'; import { View, Text, TextInput, Pressable, FlatList, ActivityIndicator, StyleSheet, RefreshControl, KeyboardAvoidingView, Platform, } from 'react-native'; import { toast } from 'sonner-native'; import { CheckCircle2, Circle, Trash2, Plus, RefreshCw, Edit2, X, Check, Activity, Database, HardDrive, } from 'lucide-react-native'; import { getApiUrl } from '../../utils/api'; type Todo = { id: number; title: string; description: string | null; completed: boolean; createdAt: string; updatedAt: string; attachment: unknown | null; }; type HealthResponse = { status: string; projectId: string; ready: boolean; config: { apiPrefix: string; databaseConfigured: boolean; storageConfigured: boolean; missingEnv: string[]; }; }; export default function Index() { const [todos, setTodos] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Form State const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); const [submitting, setSubmitting] = useState(false); // Edit State const [editingId, setEditingId] = useState(null); const [editTitle, setEditTitle] = useState(''); const [editDescription, setEditDescription] = useState(''); // Health State const [health, setHealth] = useState(null); const [healthLoading, setHealthLoading] = useState(true); // Fetch Health Check Status const fetchHealth = useCallback(async () => { try { setHealthLoading(true); const res = await fetch(getApiUrl('/health')); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); setHealth(data); } catch (err) { console.warn('Health check failed', err); setHealth(null); } finally { setHealthLoading(false); } }, []); // Fetch Todos List const fetchTodos = useCallback(async () => { try { setLoading(true); setError(null); const res = await fetch(getApiUrl('/todos')); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); setTodos(data.todos ?? []); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to fetch todos'); toast.error('Fetch Failed', { description: 'Could not retrieve todo items from backend.', }); } finally { setLoading(false); } }, []); // Initial Load useEffect(() => { fetchHealth(); fetchTodos(); }, [fetchHealth, fetchTodos]); const handleRefresh = async () => { await Promise.all([fetchHealth(), fetchTodos()]); toast.success('Refreshed', { description: 'Dashboard updated successfully.', }); }; // Create Todo const createTodo = async () => { if (!title.trim()) return; try { setSubmitting(true); const res = await fetch(getApiUrl('/todos'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: title.trim(), description: description.trim() || null, }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); setTitle(''); setDescription(''); toast.success('Success', { description: 'Todo item created.', }); await fetchTodos(); } catch (err) { toast.error('Error', { description: 'Failed to create todo item.', }); } finally { setSubmitting(false); } }; // Toggle Completed const toggleCompleted = async (todo: Todo) => { try { const res = await fetch(getApiUrl(`/todos/${todo.id}`), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ completed: !todo.completed }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); toast.success(todo.completed ? 'Reopened' : 'Completed', { description: `"${todo.title}" updated.`, }); await fetchTodos(); } catch (err) { toast.error('Error', { description: 'Failed to update todo status.', }); } }; // Update Title/Description const saveEdit = async (id: number) => { if (!editTitle.trim()) return; try { const res = await fetch(getApiUrl(`/todos/${id}`), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: editTitle.trim(), description: editDescription.trim() || null, }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); setEditingId(null); toast.success('Updated', { description: 'Todo changes saved.', }); await fetchTodos(); } catch (err) { toast.error('Error', { description: 'Failed to update todo details.', }); } }; // Delete Todo const deleteTodo = async (id: number) => { try { const res = await fetch(getApiUrl(`/todos/${id}`), { method: 'DELETE', }); if (!res.ok) throw new Error(`HTTP ${res.status}`); toast.success('Deleted', { description: 'Todo item removed.', }); await fetchTodos(); } catch (err) { toast.error('Error', { description: 'Failed to delete todo.', }); } }; return ( {/* Header Banner */} Todo Workspace {/* Health Check Panel */} {healthLoading ? ( Verifying system health... ) : health ? ( Status: {health.status.toUpperCase()} DB Storage ) : ( Backend Offline )} {/* Main Content List */} item.id.toString()} refreshControl={ } contentContainerStyle={styles.listContent} ListHeaderComponent={ /* Add Todo Panel */ Add New Task {submitting ? ( ) : ( <> Create Todo )} } ListEmptyComponent={ !loading ? ( No tasks yet. Create one above! ) : null } renderItem={({ item }) => ( {editingId === item.id ? ( /* Editing State */ saveEdit(item.id)} > setEditingId(null)} > ) : ( /* Display State */ toggleCompleted(item)} style={styles.checkboxContainer}> {item.completed ? ( ) : ( )} {item.title} {item.description ? ( {item.description} ) : null} { setEditingId(item.id); setEditTitle(item.title); setEditDescription(item.description ?? ''); }} > deleteTodo(item.id)}> )} )} /> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#F9FAFB', }, header: { backgroundColor: '#FFFFFF', paddingTop: 50, paddingHorizontal: 20, paddingBottom: 16, borderBottomWidth: 1, borderBottomColor: '#E5E7EB', }, headerRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, }, headerTitle: { fontSize: 22, fontWeight: 'bold', color: '#111827', }, refreshBtn: { padding: 8, borderRadius: 8, backgroundColor: '#EEF2F6', }, healthContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 8, paddingHorizontal: 12, borderRadius: 8, backgroundColor: '#F3F4F6', }, healthOk: { backgroundColor: '#ECFDF5', }, healthErr: { backgroundColor: '#FEF2F2', }, healthRow: { flexDirection: 'row', alignItems: 'center', gap: 6, }, healthStatusText: { fontSize: 12, fontWeight: '600', color: '#374151', }, healthIcons: { flexDirection: 'row', gap: 12, }, healthIconItem: { flexDirection: 'row', alignItems: 'center', gap: 4, }, healthIconLabel: { fontSize: 10, fontWeight: '500', color: '#6B7280', }, healthText: { fontSize: 12, color: '#6B7280', marginLeft: 8, }, listContent: { padding: 20, paddingBottom: 40, }, formContainer: { backgroundColor: '#FFFFFF', borderRadius: 12, padding: 16, marginBottom: 20, borderWidth: 1, borderColor: '#E5E7EB', shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.05, shadowRadius: 2, elevation: 2, }, sectionTitle: { fontSize: 16, fontWeight: '600', color: '#374151', marginBottom: 12, }, input: { backgroundColor: '#F9FAFB', borderWidth: 1, borderColor: '#D1D5DB', borderRadius: 8, paddingHorizontal: 12, paddingVertical: 8, fontSize: 14, color: '#111827', marginBottom: 10, }, textArea: { height: 60, textAlignVertical: 'top', }, createBtn: { backgroundColor: '#4F46E5', flexDirection: 'row', justifyContent: 'center', alignItems: 'center', paddingVertical: 10, borderRadius: 8, marginTop: 4, }, createBtnDisabled: { backgroundColor: '#93C5FD', }, btnIcon: { marginRight: 6, }, createBtnText: { color: '#FFFFFF', fontWeight: '600', fontSize: 14, }, todoCard: { backgroundColor: '#FFFFFF', borderRadius: 12, padding: 16, marginBottom: 12, borderWidth: 1, borderColor: '#E5E7EB', shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.03, shadowRadius: 2, elevation: 1, }, todoCardCompleted: { backgroundColor: '#FAFAFA', borderColor: '#E5E7EB', }, todoRow: { flexDirection: 'row', alignItems: 'flex-start', }, checkboxContainer: { marginRight: 12, marginTop: 2, }, todoTexts: { flex: 1, marginRight: 8, }, todoTitle: { fontSize: 15, fontWeight: '600', color: '#111827', marginBottom: 4, }, todoDesc: { fontSize: 13, color: '#6B7280', }, lineThrough: { textDecorationLine: 'line-through', color: '#9CA3AF', }, todoActions: { flexDirection: 'row', alignItems: 'center', gap: 8, }, actionIconBtn: { padding: 6, borderRadius: 6, backgroundColor: '#F3F4F6', }, editForm: { gap: 8, }, editInput: { backgroundColor: '#F9FAFB', borderWidth: 1, borderColor: '#D1D5DB', borderRadius: 6, paddingHorizontal: 10, paddingVertical: 6, fontSize: 14, color: '#111827', }, editActions: { flexDirection: 'row', justifyContent: 'flex-end', gap: 8, marginTop: 4, }, iconBtn: { paddingVertical: 6, paddingHorizontal: 12, borderRadius: 6, justifyContent: 'center', alignItems: 'center', }, saveBtn: { backgroundColor: '#10B981', }, cancelBtn: { backgroundColor: '#EF4444', }, emptyContainer: { alignItems: 'center', paddingVertical: 40, }, emptyText: { fontSize: 14, color: '#9CA3AF', }, });