Skip to content

Instantly share code, notes, and snippets.

@gtchakama
Created December 12, 2024 21:37
Show Gist options
  • Save gtchakama/ab432787382bc8e9d15a4b8b5b01e9ef to your computer and use it in GitHub Desktop.
Save gtchakama/ab432787382bc8e9d15a4b8b5b01e9ef to your computer and use it in GitHub Desktop.
Todo App
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