Created
December 12, 2024 21:37
-
-
Save gtchakama/ab432787382bc8e9d15a4b8b5b01e9ef to your computer and use it in GitHub Desktop.
Todo App
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useState, useEffect, useCallback } from 'react'; | |
import { | |
View, | |
Text, | |
ScrollView, | |
StyleSheet, | |
SafeAreaView, | |
TouchableOpacity, | |
StatusBar, | |
ActivityIndicator, | |
TextInput, | |
Animated, | |
Modal, | |
Alert, | |
Dimensions | |
} from 'react-native'; | |
import { MaterialIcons } from '@expo/vector-icons'; | |
export default function Index() { | |
const [tasks, setTasks] = useState([]); | |
const [loading, setLoading] = useState(true); | |
const [newTaskText, setNewTaskText] = useState(''); | |
const [searchQuery, setSearchQuery] = useState(''); | |
const [filterCompleted, setFilterCompleted] = useState('all'); | |
const [isAddingTask, setIsAddingTask] = useState(false); | |
const [sortBy, setSortBy] = useState('date'); | |
const [sortDirection, setSortDirection] = useState('desc'); | |
const [selectedTask, setSelectedTask] = useState(null); | |
const [showTaskModal, setShowTaskModal] = useState(false); | |
const [taskStats, setTaskStats] = useState({ | |
total: 0, | |
completed: 0, | |
pending: 0 | |
}); | |
const fadeAnim = new Animated.Value(1); | |
useEffect(() => { | |
fetchTasks(); | |
}, []); | |
const toggleAddTaskView = (show) => { | |
Animated.timing(fadeAnim, { | |
toValue: 0, | |
duration: 150, | |
useNativeDriver: true, | |
}).start(() => { | |
setIsAddingTask(show); | |
Animated.timing(fadeAnim, { | |
toValue: 1, | |
duration: 150, | |
useNativeDriver: true, | |
}).start(); | |
}); | |
}; | |
const fetchTasks = async () => { | |
try { | |
const response = await fetch('https://jsonplaceholder.typicode.com/todos'); | |
const data = await response.json(); | |
const formattedTasks = data.slice(0, 10).map(task => ({ | |
...task, | |
isCompleted: false, | |
createdAt: new Date().toISOString(), | |
priority: ['low', 'medium', 'high'][Math.floor(Math.random() * 3)], | |
dueDate: new Date(Date.now() + Math.random() * 7 * 24 * 60 * 60 * 1000).toISOString(), | |
notes: '' | |
})); | |
setTasks(formattedTasks); | |
updateTaskStats(formattedTasks); | |
setLoading(false); | |
} catch (error) { | |
console.error('Error fetching tasks:', error); | |
setLoading(false); | |
} | |
}; | |
const updateTaskStats = useCallback((currentTasks) => { | |
setTaskStats({ | |
total: currentTasks.length, | |
completed: currentTasks.filter(task => task.isCompleted).length, | |
pending: currentTasks.filter(task => !task.isCompleted).length | |
}); | |
}, []); | |
const handleMarkComplete = (taskId) => { | |
setTasks(prevTasks => { | |
const newTasks = prevTasks.map(task => | |
task.id === taskId ? { ...task, isCompleted: !task.isCompleted } : task | |
); | |
updateTaskStats(newTasks); | |
return newTasks; | |
}); | |
}; | |
const handleAddTask = () => { | |
if (newTaskText.trim()) { | |
const newTask = { | |
id: Date.now(), | |
title: newTaskText, | |
isCompleted: false, | |
createdAt: new Date().toISOString(), | |
priority: 'medium', | |
dueDate: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), | |
notes: '' | |
}; | |
setTasks(prev => { | |
const newTasks = [newTask, ...prev]; | |
updateTaskStats(newTasks); | |
return newTasks; | |
}); | |
setNewTaskText(''); | |
toggleAddTaskView(false); | |
} | |
}; | |
const handleDeleteTask = (taskId) => { | |
Alert.alert( | |
"Delete Task", | |
"Are you sure you want to delete this task?", | |
[ | |
{ | |
text: "Cancel", | |
style: "cancel" | |
}, | |
{ | |
text: "Delete", | |
onPress: () => { | |
setTasks(prev => { | |
const newTasks = prev.filter(task => task.id !== taskId); | |
updateTaskStats(newTasks); | |
return newTasks; | |
}); | |
}, | |
style: "destructive" | |
} | |
] | |
); | |
}; | |
const handleUpdateTask = (updatedTask) => { | |
setTasks(prev => prev.map(task => | |
task.id === updatedTask.id ? updatedTask : task | |
)); | |
setShowTaskModal(false); | |
}; | |
const sortTasks = (tasksToSort) => { | |
return tasksToSort.sort((a, b) => { | |
switch (sortBy) { | |
case 'date': | |
return sortDirection === 'desc' | |
? new Date(b.createdAt) - new Date(a.createdAt) | |
: new Date(a.createdAt) - new Date(b.createdAt); | |
case 'priority': | |
const priorityOrder = { high: 3, medium: 2, low: 1 }; | |
return sortDirection === 'desc' | |
? priorityOrder[b.priority] - priorityOrder[a.priority] | |
: priorityOrder[a.priority] - priorityOrder[b.priority]; | |
case 'alphabetical': | |
return sortDirection === 'desc' | |
? b.title.localeCompare(a.title) | |
: a.title.localeCompare(b.title); | |
default: | |
return 0; | |
} | |
}); | |
}; | |
const filteredTasks = sortTasks( | |
tasks.filter(task => { | |
if (filterCompleted === 'completed') return task.isCompleted; | |
if (filterCompleted === 'active') return !task.isCompleted; | |
return true; | |
}).filter(task => | |
task.title.toLowerCase().includes(searchQuery.toLowerCase()) | |
) | |
); | |
const AddTaskView = () => ( | |
<View style={styles.addTaskView}> | |
<View style={styles.addTaskHeader}> | |
<Text style={styles.addTaskTitle}>Add New Task</Text> | |
<TouchableOpacity | |
style={styles.closeButton} | |
onPress={() => toggleAddTaskView(false)} | |
> | |
<MaterialIcons name="close" size={24} color="#1a237e" /> | |
</TouchableOpacity> | |
</View> | |
<TextInput | |
style={styles.addTaskInput} | |
placeholder="Enter task title..." | |
value={newTaskText} | |
onChangeText={setNewTaskText} | |
autoFocus | |
multiline | |
/> | |
<TouchableOpacity | |
style={styles.submitButton} | |
onPress={handleAddTask} | |
> | |
<Text style={styles.submitButtonText}>Add Task</Text> | |
</TouchableOpacity> | |
</View> | |
); | |
const TaskModal = () => ( | |
<Modal | |
visible={showTaskModal} | |
animationType="slide" | |
transparent={true} | |
onRequestClose={() => setShowTaskModal(false)} | |
> | |
<View style={styles.modalContainer}> | |
<View style={styles.modalContent}> | |
<Text style={styles.modalTitle}>Task Details</Text> | |
<TextInput | |
style={styles.modalInput} | |
value={selectedTask?.title} | |
onChangeText={(text) => setSelectedTask(prev => ({ ...prev, title: text }))} | |
placeholder="Task title" | |
/> | |
<View style={styles.prioritySelector}> | |
{['low', 'medium', 'high'].map(priority => ( | |
<TouchableOpacity | |
key={priority} | |
style={[ | |
styles.priorityButton, | |
selectedTask?.priority === priority && { | |
backgroundColor: getPriorityColor(priority) | |
} | |
]} | |
onPress={() => setSelectedTask(prev => ({ ...prev, priority }))} | |
> | |
<Text style={[ | |
styles.priorityButtonText, | |
selectedTask?.priority === priority && styles.selectedPriorityText | |
]}> | |
{priority.charAt(0).toUpperCase() + priority.slice(1)} | |
</Text> | |
</TouchableOpacity> | |
))} | |
</View> | |
<TextInput | |
style={[styles.modalInput, styles.notesInput]} | |
value={selectedTask?.notes} | |
onChangeText={(text) => setSelectedTask(prev => ({ ...prev, notes: text }))} | |
placeholder="Add notes..." | |
multiline | |
/> | |
<View style={styles.modalButtons}> | |
<TouchableOpacity | |
style={[styles.modalButton, styles.cancelButton]} | |
onPress={() => setShowTaskModal(false)} | |
> | |
<Text style={styles.modalButtonText}>Cancel</Text> | |
</TouchableOpacity> | |
<TouchableOpacity | |
style={[styles.modalButton, styles.saveButton]} | |
onPress={() => handleUpdateTask(selectedTask)} | |
> | |
<Text style={styles.modalButtonText}>Save</Text> | |
</TouchableOpacity> | |
</View> | |
</View> | |
</View> | |
</Modal> | |
); | |
const ListView = () => ( | |
<> | |
<View style={styles.controlsContainer}> | |
<View style={styles.searchBar}> | |
<MaterialIcons name="search" size={20} color="#757575" style={styles.searchIcon} /> | |
<TextInput | |
style={styles.searchInput} | |
placeholder="Search tasks..." | |
value={searchQuery} | |
onChangeText={setSearchQuery} | |
/> | |
</View> | |
<View style={styles.filterSort}> | |
<TouchableOpacity | |
style={styles.filterButton} | |
onPress={() => { | |
setFilterCompleted(current => { | |
if (current === 'all') return 'active'; | |
if (current === 'active') return 'completed'; | |
return 'all'; | |
}); | |
}} | |
> | |
<MaterialIcons name="filter-list" size={20} color="#757575" /> | |
<Text style={styles.filterButtonText}> | |
{filterCompleted.charAt(0).toUpperCase() + filterCompleted.slice(1)} | |
</Text> | |
</TouchableOpacity> | |
<TouchableOpacity | |
style={styles.sortButton} | |
onPress={() => { | |
setSortBy(current => { | |
if (current === 'date') return 'priority'; | |
if (current === 'priority') return 'alphabetical'; | |
return 'date'; | |
}); | |
}} | |
> | |
<MaterialIcons | |
name={sortDirection === 'desc' ? "arrow-downward" : "arrow-upward"} | |
size={20} | |
color="#757575" | |
onPress={() => setSortDirection(current => current === 'desc' ? 'asc' : 'desc')} | |
/> | |
<Text style={styles.sortButtonText}> | |
{sortBy.charAt(0).toUpperCase() + sortBy.slice(1)} | |
</Text> | |
</TouchableOpacity> | |
</View> | |
</View> | |
<ScrollView style={styles.scrollView}> | |
<View style={styles.taskContainer}> | |
{filteredTasks.map(task => ( | |
<TouchableOpacity | |
key={task.id} | |
onPress={() => { | |
setSelectedTask(task); | |
setShowTaskModal(true); | |
}} | |
> | |
<View | |
style={[ | |
styles.taskCard, | |
task.isCompleted && styles.completedTask | |
]} | |
> | |
<View style={styles.taskContent}> | |
<TouchableOpacity | |
style={styles.taskCheckbox} | |
onPress={(e) => { | |
e.stopPropagation(); | |
handleMarkComplete(task.id); | |
}} | |
> | |
<MaterialIcons | |
name={task.isCompleted ? "check-circle" : "radio-button-unchecked"} | |
size={24} | |
color={task.isCompleted ? "#4CAF50" : "#9e9e9e"} | |
/> | |
</TouchableOpacity> | |
<View style={styles.taskInfo}> | |
<Text style={[ | |
styles.taskTitle, | |
task.isCompleted && styles.completedTaskText | |
]}> | |
{task.title} | |
</Text> | |
<View style={styles.taskMeta}> | |
<View style={[ | |
styles.priorityTag, | |
{ backgroundColor: getPriorityColor(task.priority) } | |
]}> | |
<Text style={styles.priorityText}> | |
{task.priority} | |
</Text> | |
</View> | |
<Text style={styles.taskDate}> | |
Due: {new Date(task.dueDate).toLocaleDateString()} | |
</Text> | |
</View> | |
{task.notes && ( | |
<Text style={styles.taskNotes} numberOfLines={1}> | |
{task.notes} | |
</Text> | |
)} | |
</View> | |
<TouchableOpacity | |
style={styles.deleteButton} | |
onPress={(e) => { | |
e.stopPropagation(); | |
handleDeleteTask(task.id); | |
}} | |
> | |
<MaterialIcons name="delete-outline" size={20} color="#f44336" /> | |
</TouchableOpacity> | |
</View> | |
</View> | |
</TouchableOpacity> | |
))} | |
</View> | |
</ScrollView> | |
</> | |
); | |
const getPriorityColor = (priority) => { | |
switch (priority) { | |
case 'high': return '#f44336'; | |
case 'medium': return '#ff9800'; | |
case 'low': return '#4caf50'; | |
default: return '#757575'; | |
} | |
}; | |
return ( | |
<SafeAreaView style={styles.container}> | |
<StatusBar barStyle="light-content" backgroundColor="#1a237e" /> | |
<View style={styles.header}> | |
<View style={styles.headerContent}> | |
<Text style={styles.headerTitle}>My Tasks</Text> | |
<TouchableOpacity | |
style={styles.addButton} | |
onPress={() => toggleAddTaskView(!isAddingTask)} | |
> | |
<MaterialIcons name={isAddingTask ? "close" : "add"} size={24} color="#ffffff" /> | |
</TouchableOpacity> | |
</View> | |
<View style={styles.statsContainer}> | |
<View style={styles.statItem}> | |
<Text style={styles.statNumber}>{taskStats.total}</Text> | |
<Text style={styles.statLabel}>Total</Text> | |
</View> | |
<View style={styles.statItem}> | |
<Text style={styles.statNumber}>{taskStats.completed}</Text> | |
<Text style={styles.statLabel}>Done</Text> | |
</View> | |
<View style={styles.statItem}> | |
<Text style={styles.statNumber}>{taskStats.pending}</Text> | |
<Text style={styles.statLabel}>Pending</Text> | |
</View> | |
</View> | |
</View> | |
{loading ? ( | |
<View style={styles.loadingContainer}> | |
<ActivityIndicator size="large" color="#1a237e" /> | |
</View> | |
) : ( | |
<Animated.View style={[styles.mainContent, { opacity: fadeAnim }]}> | |
{isAddingTask ? <AddTaskView /> : <ListView />} | |
</Animated.View> | |
)} | |
<TaskModal /> | |
</SafeAreaView> | |
); | |
} | |
const styles = StyleSheet.create({ | |
container: { | |
flex: 1, | |
backgroundColor: '#f5f5f5', | |
}, | |
header: { | |
backgroundColor: '#1a237e', | |
paddingTop: StatusBar.currentHeight || 0, | |
elevation: 4, | |
shadowColor: '#000', | |
shadowOffset: { width: 0, height: 2 }, | |
shadowOpacity: 0.2, | |
shadowRadius: 4, | |
}, | |
headerContent: { | |
flexDirection: 'row', | |
alignItems: 'center', | |
justifyContent: 'space-between', | |
paddingHorizontal: 16, | |
paddingTop: 16, | |
}, | |
headerTitle: { | |
color: '#ffffff', | |
fontSize: 28, | |
fontWeight: '600', | |
}, | |
addButton: { | |
padding: 8, | |
}, | |
statsContainer: { | |
flexDirection: 'row', | |
justifyContent: 'space-around', | |
paddingVertical: 12, | |
marginTop: 8, | |
}, | |
statItem: { | |
alignItems: 'center', | |
}, | |
statNumber: { | |
color: '#ffffff', | |
fontSize: 20, | |
fontWeight: '600', | |
}, | |
statLabel: { | |
color: '#ffffff', | |
fontSize: 12, | |
opacity: 0.8, | |
}, | |
mainContent: { | |
flex: 1, | |
}, | |
addTaskView: { | |
flex: 1, | |
backgroundColor: '#ffffff', | |
padding: 16, | |
}, | |
addTaskHeader: { | |
flexDirection: 'row', | |
justifyContent: 'space-between', | |
alignItems: 'center', | |
marginBottom: 24, | |
}, | |
addTaskTitle: { | |
fontSize: 20, | |
fontWeight: '600', | |
color: '#1a237e', | |
}, | |
closeButton: { | |
padding: 8, | |
}, | |
addTaskInput: { | |
borderWidth: 1, | |
borderColor: '#e0e0e0', | |
borderRadius: 8, | |
padding: 16, | |
fontSize: 16, | |
minHeight: 120, | |
textAlignVertical: 'top', | |
marginBottom: 24, | |
}, | |
submitButton: { | |
backgroundColor: '#1a237e', | |
padding: 16, | |
borderRadius: 8, | |
alignItems: 'center', | |
}, | |
submitButtonText: { | |
color: '#ffffff', | |
fontSize: 16, | |
fontWeight: '600', | |
}, | |
controlsContainer: { | |
backgroundColor: '#ffffff', | |
padding: 16, | |
elevation: 2, | |
shadowColor: '#000', | |
shadowOffset: { width: 0, height: 1 }, | |
shadowOpacity: 0.1, | |
shadowRadius: 2, | |
}, | |
searchBar: { | |
flexDirection: 'row', | |
alignItems: 'center', | |
backgroundColor: '#f5f5f5', | |
borderRadius: 8, | |
paddingHorizontal: 12, | |
marginBottom: 12, | |
}, | |
searchIcon: { | |
marginRight: 8, | |
}, | |
searchInput: { | |
flex: 1, | |
height: 40, | |
fontSize: 16, | |
}, | |
filterSort: { | |
flexDirection: 'row', | |
justifyContent: 'space-between', | |
}, | |
filterButton: { | |
flexDirection: 'row', | |
alignItems: 'center', | |
backgroundColor: '#f5f5f5', | |
padding: 8, | |
borderRadius: 8, | |
}, | |
filterButtonText: { | |
marginLeft: 4, | |
color: '#757575', | |
fontSize: 14, | |
}, | |
sortButton: { | |
flexDirection: 'row', | |
alignItems: 'center', | |
backgroundColor: '#f5f5f5', | |
padding: 8, | |
borderRadius: 8, | |
}, | |
sortButtonText: { | |
marginLeft: 4, | |
color: '#757575', | |
fontSize: 14, | |
}, | |
scrollView: { | |
flex: 1, | |
}, | |
taskContainer: { | |
padding: 8, | |
}, | |
taskCard: { | |
backgroundColor: '#ffffff', | |
borderRadius: 8, | |
marginVertical: 4, | |
elevation: 2, | |
shadowColor: '#000', | |
shadowOffset: { width: 0, height: 1 }, | |
shadowOpacity: 0.1, | |
shadowRadius: 2, | |
}, | |
completedTask: { | |
backgroundColor: '#f8f9fa', | |
}, | |
taskContent: { | |
flexDirection: 'row', | |
alignItems: 'center', | |
padding: 16, | |
}, | |
taskCheckbox: { | |
marginRight: 12, | |
}, | |
taskInfo: { | |
flex: 1, | |
}, | |
taskTitle: { | |
fontSize: 16, | |
color: '#212121', | |
marginBottom: 4, | |
fontWeight: '500', | |
}, | |
completedTaskText: { | |
textDecorationLine: 'line-through', | |
color: '#9e9e9e', | |
}, | |
taskMeta: { | |
flexDirection: 'row', | |
alignItems: 'center', | |
marginBottom: 4, | |
}, | |
priorityTag: { | |
paddingHorizontal: 8, | |
paddingVertical: 2, | |
borderRadius: 4, | |
marginRight: 8, | |
}, | |
priorityText: { | |
color: '#ffffff', | |
fontSize: 12, | |
fontWeight: '500', | |
}, | |
taskDate: { | |
fontSize: 12, | |
color: '#757575', | |
}, | |
taskNotes: { | |
fontSize: 12, | |
color: '#757575', | |
fontStyle: 'italic', | |
}, | |
deleteButton: { | |
padding: 8, | |
}, | |
modalContainer: { | |
flex: 1, | |
backgroundColor: 'rgba(0, 0, 0, 0.5)', | |
justifyContent: 'center', | |
padding: 16, | |
}, | |
modalContent: { | |
backgroundColor: '#ffffff', | |
borderRadius: 8, | |
padding: 16, | |
elevation: 5, | |
shadowColor: '#000', | |
shadowOffset: { width: 0, height: 2 }, | |
shadowOpacity: 0.25, | |
shadowRadius: 4, | |
}, | |
modalTitle: { | |
fontSize: 20, | |
fontWeight: '600', | |
marginBottom: 16, | |
color: '#1a237e', | |
}, | |
modalInput: { | |
borderWidth: 1, | |
borderColor: '#e0e0e0', | |
borderRadius: 4, | |
padding: 8, | |
marginBottom: 16, | |
fontSize: 16, | |
}, | |
notesInput: { | |
height: 80, | |
textAlignVertical: 'top', | |
}, | |
prioritySelector: { | |
flexDirection: 'row', | |
justifyContent: 'space-between', | |
marginBottom: 16, | |
}, | |
priorityButton: { | |
flex: 1, | |
padding: 8, | |
borderRadius: 4, | |
borderWidth: 1, | |
borderColor: '#e0e0e0', | |
marginHorizontal: 4, | |
alignItems: 'center', | |
}, | |
priorityButtonText: { | |
color: '#757575', | |
fontSize: 14, | |
}, | |
selectedPriorityText: { | |
color: '#ffffff', | |
fontWeight: '500', | |
}, | |
modalButtons: { | |
flexDirection: 'row', | |
justifyContent: 'flex-end', | |
}, | |
modalButton: { | |
paddingHorizontal: 16, | |
paddingVertical: 8, | |
borderRadius: 4, | |
marginLeft: 8, | |
}, | |
cancelButton: { | |
backgroundColor: '#757575', | |
}, | |
saveButton: { | |
backgroundColor: '#1a237e', | |
}, | |
modalButtonText: { | |
color: '#ffffff', | |
fontSize: 14, | |
fontWeight: '500', | |
}, | |
loadingContainer: { | |
flex: 1, | |
justifyContent: 'center', | |
alignItems: 'center', | |
}, | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment