organize
This commit is contained in:
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user