This commit is contained in:
2026-02-26 20:49:21 -05:00
parent bebcf48a32
commit 4d9e715361
243 changed files with 25648 additions and 14573 deletions
@@ -0,0 +1,49 @@
import React from 'react';
import { View, TouchableOpacity, Text, StyleSheet } from 'react-native';
import { categories } from '../state';
import { colors } from '../theme';
export default function CategoryPicker({ selected, onSelect }) {
return (
<View style={styles.container}>
{categories.map(cat => {
const isSelected = cat === selected;
return (
<TouchableOpacity
key={cat}
onPress={() => onSelect(cat)}
style={[
styles.pill,
{
backgroundColor: isSelected ? colors.primary : colors.border,
},
]}
>
<Text
style={{
color: isSelected ? colors.onPrimary : colors.onSurface,
fontSize: 14,
}}
>
{cat}
</Text>
</TouchableOpacity>
);
})}
</View>
);
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
marginVertical: 8,
},
pill: {
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 20,
},
});
+59
View File
@@ -0,0 +1,59 @@
import React from 'react';
import { View, TouchableOpacity, Text, StyleSheet } from 'react-native';
import { useTodoState, getTodoCounts } from '../state';
import { colors } from '../theme';
const filters = [
{ key: 'all', label: 'All' },
{ key: 'active', label: 'Active' },
{ key: 'completed', label: 'Done' },
];
export default function FilterBar() {
const { state, dispatch } = useTodoState();
const counts = getTodoCounts(state);
return (
<View style={styles.container}>
{filters.map(({ key, label }) => {
const isActive = state.filter === key;
return (
<TouchableOpacity
key={key}
onPress={() => dispatch({ type: 'SET_FILTER', filter: key })}
style={[
styles.pill,
{ backgroundColor: isActive ? colors.primary : colors.border },
]}
>
<Text
style={[
styles.label,
{ color: isActive ? colors.onPrimary : colors.textSecondary },
]}
>
{label} ({counts[key] || 0})
</Text>
</TouchableOpacity>
);
})}
</View>
);
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
paddingHorizontal: 16,
paddingVertical: 8,
gap: 8,
},
pill: {
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 20,
},
label: {
fontWeight: '600',
},
});
+110
View File
@@ -0,0 +1,110 @@
import React from 'react';
import { View, Text, TouchableOpacity, Pressable, FlatList, StyleSheet } from 'react-native';
import { useTodoState, getFilteredTodos } from '../state';
import { colors } from '../theme';
import FilterBar from './FilterBar';
import TodoItem from './TodoItem';
function EmptyState() {
return (
<View style={styles.emptyContainer}>
<Text style={styles.emptyIcon}>{'\uD83D\uDCDD'}</Text>
<Text style={styles.emptyText}>{'No todos yet!\nTap + to add one'}</Text>
</View>
);
}
function TodoList() {
const { state } = useTodoState();
const todos = getFilteredTodos(state);
if (todos.length === 0) {
return <EmptyState />;
}
return (
<FlatList
data={todos}
renderItem={({ item }) => <TodoItem todo={item} />}
keyExtractor={item => String(item.id)}
contentContainerStyle={{ paddingBottom: 100 }}
/>
);
}
export default function MainScreen() {
const { dispatch } = useTodoState();
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.headerText}>Todos JS Expo</Text>
</View>
<FilterBar />
<View style={{ flex: 1 }}>
<TodoList />
</View>
<Pressable
onPress={() => dispatch({ type: 'SHOW_ADD_FORM' })}
style={styles.fab}
>
<Text style={styles.fabText}>+</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
header: {
backgroundColor: colors.primary,
paddingTop: 48,
paddingBottom: 16,
paddingHorizontal: 24,
},
headerText: {
fontSize: 28,
fontWeight: 'bold',
color: colors.onPrimary,
},
fab: {
position: 'absolute',
right: 24,
bottom: 32,
zIndex: 10,
width: 56,
height: 56,
borderRadius: 28,
backgroundColor: colors.primary,
justifyContent: 'center',
alignItems: 'center',
elevation: 6,
shadowColor: '#000',
shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.3,
shadowRadius: 4,
},
fabText: {
color: '#FFFFFF',
fontSize: 28,
lineHeight: 30,
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 48,
},
emptyIcon: {
fontSize: 48,
marginBottom: 16,
},
emptyText: {
fontSize: 18,
color: colors.textSecondary,
textAlign: 'center',
},
});
@@ -0,0 +1,50 @@
import React from 'react';
import { View, TouchableOpacity, Text, StyleSheet } from 'react-native';
import { priorities } from '../state';
export default function PriorityPicker({ selected, onSelect }) {
return (
<View style={styles.container}>
{priorities.map(({ key, label, color }) => {
const isSelected = key === selected;
return (
<TouchableOpacity
key={key}
onPress={() => onSelect(key)}
style={[
styles.pill,
{
borderColor: color,
backgroundColor: isSelected ? color : 'transparent',
},
]}
>
<Text
style={{
color: isSelected ? '#FFFFFF' : color,
fontWeight: '600',
fontSize: 14,
}}
>
{label}
</Text>
</TouchableOpacity>
);
})}
</View>
);
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
gap: 8,
marginVertical: 8,
},
pill: {
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 20,
borderWidth: 2,
},
});
+156
View File
@@ -0,0 +1,156 @@
import React, { useState } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
} from 'react-native';
import { useTodoState } from '../state';
import { colors } from '../theme';
import * as db from '../db';
import CategoryPicker from './CategoryPicker';
import PriorityPicker from './PriorityPicker';
export default function TodoForm() {
const { state, dispatch } = useTodoState();
const editing = state.editingTodo;
const [title, setTitle] = useState(editing?.title || '');
const [category, setCategory] = useState(editing?.category || 'Personal');
const [priority, setPriority] = useState(editing?.priority || 'medium');
const handleSubmit = async () => {
const trimmed = title.trim();
if (!trimmed) return;
if (editing) {
dispatch({
type: 'UPDATE_TODO',
id: editing.id,
title: trimmed,
category,
priority,
});
db.updateTodo(editing.id, trimmed, category, priority).catch(err =>
console.error('Update error:', err)
);
} else {
try {
const id = await db.insertTodo(trimmed, category, priority);
dispatch({
type: 'TODO_INSERTED',
id,
title: trimmed,
category,
priority,
});
} catch (err) {
console.error('Insert error:', err);
}
}
};
const handleCancel = () => {
dispatch({ type: 'HIDE_FORM' });
};
return (
<View style={styles.overlay}>
<View style={styles.card}>
<Text style={styles.heading}>
{editing ? 'Edit Todo' : 'Add New Todo'}
</Text>
<TextInput
style={styles.input}
placeholder="What needs to be done?"
placeholderTextColor={colors.textSecondary}
value={title}
onChangeText={setTitle}
autoFocus
/>
<Text style={styles.sectionLabel}>Category</Text>
<CategoryPicker selected={category} onSelect={setCategory} />
<Text style={[styles.sectionLabel, { marginTop: 8 }]}>Priority</Text>
<PriorityPicker selected={priority} onSelect={setPriority} />
<View style={styles.actions}>
<TouchableOpacity onPress={handleCancel} style={styles.cancelBtn}>
<Text style={{ color: colors.textSecondary, fontSize: 16 }}>
Cancel
</Text>
</TouchableOpacity>
<TouchableOpacity onPress={handleSubmit} style={styles.submitBtn}>
<Text style={styles.submitText}>
{editing ? 'Save' : 'Add'}
</Text>
</TouchableOpacity>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
overlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
justifyContent: 'center',
padding: 24,
},
card: {
backgroundColor: colors.surface,
borderRadius: 16,
padding: 24,
elevation: 8,
},
heading: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 16,
color: colors.onSurface,
},
input: {
borderWidth: 1,
borderColor: colors.border,
borderRadius: 8,
padding: 12,
fontSize: 16,
marginBottom: 16,
},
sectionLabel: {
fontSize: 14,
fontWeight: '600',
color: colors.textSecondary,
marginBottom: 4,
},
actions: {
flexDirection: 'row',
justifyContent: 'flex-end',
gap: 12,
marginTop: 24,
},
cancelBtn: {
paddingHorizontal: 20,
paddingVertical: 10,
borderRadius: 8,
},
submitBtn: {
paddingHorizontal: 20,
paddingVertical: 10,
borderRadius: 8,
backgroundColor: colors.primary,
},
submitText: {
color: colors.onPrimary,
fontSize: 16,
fontWeight: '600',
},
});
+156
View File
@@ -0,0 +1,156 @@
import React from 'react';
import { View, TouchableOpacity, Text, StyleSheet } from 'react-native';
import { Swipeable } from 'react-native-gesture-handler';
import { useTodoState } from '../state';
import { colors, priorityColors } from '../theme';
import * as db from '../db';
export default function TodoItem({ todo }) {
const { dispatch } = useTodoState();
const { id, title, completed, category, priority } = todo;
const handleToggle = () => {
dispatch({ type: 'TOGGLE_TODO', id });
db.toggleTodo(id, !completed).catch(err =>
console.error('Toggle error:', err)
);
};
const handleEdit = () => {
dispatch({ type: 'SHOW_EDIT_FORM', todo });
};
const handleDelete = () => {
dispatch({ type: 'DELETE_TODO', id });
db.deleteTodo(id).catch(err => console.error('Delete error:', err));
};
const renderRightActions = () => (
<TouchableOpacity onPress={handleDelete} style={styles.deleteButton}>
<Text style={styles.deleteText}>Delete</Text>
</TouchableOpacity>
);
return (
<Swipeable renderRightActions={renderRightActions} overshootRight={false}>
<TouchableOpacity
onPress={handleEdit}
activeOpacity={0.7}
style={[
styles.container,
{
backgroundColor: completed ? colors.completedBg : colors.surface,
},
]}
>
<TouchableOpacity onPress={handleToggle} style={[
styles.checkbox,
{
borderColor: completed ? colors.primary : colors.border,
backgroundColor: completed ? colors.primary : 'transparent',
},
]}>
{completed && <Text style={styles.checkmark}>{'\u2713'}</Text>}
</TouchableOpacity>
<View style={styles.content}>
<Text
style={[
styles.title,
{
color: completed ? colors.completedText : colors.onSurface,
textDecorationLine: completed ? 'line-through' : 'none',
},
]}
>
{title}
</Text>
<View style={styles.meta}>
<View style={styles.categoryBadge}>
<Text style={styles.categoryText}>{category}</Text>
</View>
<View
style={[
styles.priorityDot,
{
backgroundColor:
priorityColors[priority] || '#999',
},
]}
/>
</View>
</View>
</TouchableOpacity>
</Swipeable>
);
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
marginHorizontal: 16,
marginVertical: 4,
padding: 16,
borderRadius: 12,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
checkbox: {
width: 28,
height: 28,
borderRadius: 14,
borderWidth: 2,
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
checkmark: {
color: '#FFFFFF',
fontSize: 16,
},
content: {
flex: 1,
},
title: {
fontSize: 16,
},
meta: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 4,
gap: 8,
},
categoryBadge: {
backgroundColor: '#E0E0E0',
paddingHorizontal: 8,
paddingVertical: 2,
borderRadius: 10,
},
categoryText: {
fontSize: 12,
color: '#666666',
},
priorityDot: {
width: 10,
height: 10,
borderRadius: 5,
},
deleteButton: {
backgroundColor: '#B00020',
justifyContent: 'center',
alignItems: 'center',
width: 80,
borderRadius: 12,
marginVertical: 4,
marginRight: 16,
},
deleteText: {
color: '#FFFFFF',
fontWeight: 'bold',
fontSize: 14,
},
});