Final Project
Capstone Project: TaskFlow
Build a complete production-ready task management app
šÆ Project Overview
In this capstone project, you'll build TaskFlow ā a full-featured task management app that demonstrates everything you've learned throughout this course. You'll take it from initial setup all the way to app store submission.
Project Requirements
TaskFlow is a task management application that helps users organize their work and personal tasks. The app must demonstrate proficiency in all major areas covered in this course.
Technical Requirements
š± Core
- Expo SDK 52+
- TypeScript
- Expo Router for navigation
- Proper project structure
šØ UI/UX
- Responsive layouts
- Dark mode support
- Custom components
- Meaningful animations
š Data
- Local persistence
- State management
- API integration (optional)
- Offline support
š§ Native
- Push notifications
- Haptic feedback
- At least 2 other native APIs
Minimum Viable Product (MVP)
| Feature | Required | Bonus |
|---|---|---|
| Create, edit, delete tasks | ā | |
| Task categories/projects | ā | |
| Due dates and reminders | ā | |
| Local data persistence | ā | |
| Push notifications | ā | |
| Dark mode | ā | |
| Animations (list, transitions) | ā | |
| Unit tests (>60% coverage) | ā | |
| Cloud sync | ā | |
| User authentication | ā | |
| Widgets (iOS/Android) | ā | |
| Voice input for tasks | ā |
Feature Specifications
Task Management
// Task data model
interface Task {
id: string;
title: string;
description?: string;
completed: boolean;
priority: 'low' | 'medium' | 'high';
dueDate?: Date;
reminderDate?: Date;
projectId?: string;
tags: string[];
subtasks: Subtask[];
createdAt: Date;
updatedAt: Date;
}
interface Subtask {
id: string;
title: string;
completed: boolean;
}
interface Project {
id: string;
name: string;
color: string;
icon: string;
taskCount: number;
createdAt: Date;
}
Screen Requirements
flowchart TB
subgraph Tabs["Tab Navigation"]
T1[Today]
T2[Inbox]
T3[Projects]
T4[Settings]
end
subgraph Modals["Modal Screens"]
M1[Add Task]
M2[Edit Task]
M3[Add Project]
end
subgraph Stack["Stack Screens"]
S1[Project Details]
S2[Task Details]
S3[Search]
end
T1 --> M1
T1 --> S2
T2 --> M1
T2 --> S2
T3 --> S1
T3 --> M3
S1 --> M1
S1 --> S2
Screen Descriptions
Today Screen
- Shows tasks due today
- Overdue tasks section (highlighted)
- Quick add task FAB
- Swipe to complete/delete
- Pull to refresh
Inbox Screen
- All tasks without a project
- Filter by status, priority, date
- Search functionality
- Bulk actions (select multiple)
Projects Screen
- List of all projects with task counts
- Create new project
- Drag to reorder projects
- Project color/icon customization
Settings Screen
- Theme toggle (light/dark/system)
- Notification preferences
- Default reminder time
- Data export/import
- About & version info
App Architecture
Project Structure
taskflow/
āāā app/ # Expo Router screens
ā āāā (tabs)/
ā ā āāā _layout.tsx
ā ā āāā index.tsx # Today
ā ā āāā inbox.tsx
ā ā āāā projects.tsx
ā ā āāā settings.tsx
ā āāā task/
ā ā āāā [id].tsx # Task details
ā āāā project/
ā ā āāā [id].tsx # Project details
ā āāā _layout.tsx # Root layout
ā āāā +not-found.tsx
āāā components/
ā āāā ui/ # Reusable UI components
ā ā āāā Button.tsx
ā ā āāā Input.tsx
ā ā āāā Card.tsx
ā ā āāā ...
ā āāā tasks/ # Task-related components
ā ā āāā TaskItem.tsx
ā ā āāā TaskList.tsx
ā ā āāā TaskForm.tsx
ā ā āāā ...
ā āāā projects/ # Project components
ā āāā common/ # Shared components
āāā hooks/
ā āāā useTasks.ts
ā āāā useProjects.ts
ā āāā useTheme.ts
ā āāā useNotifications.ts
āāā services/
ā āāā storage.ts # AsyncStorage wrapper
ā āāā notifications.ts # Push notification logic
ā āāā analytics.ts # Event tracking
āāā stores/ # State management (Zustand)
ā āāā taskStore.ts
ā āāā projectStore.ts
ā āāā settingsStore.ts
āāā utils/
ā āāā dates.ts
ā āāā colors.ts
ā āāā validators.ts
āāā constants/
ā āāā theme.ts
ā āāā config.ts
āāā types/
ā āāā index.ts
āāā __tests__/
State Management
// stores/taskStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface TaskState {
tasks: Task[];
isLoading: boolean;
// Actions
addTask: (task: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>) => void;
updateTask: (id: string, updates: Partial<Task>) => void;
deleteTask: (id: string) => void;
toggleComplete: (id: string) => void;
// Selectors
getTasksByProject: (projectId: string) => Task[];
getTodayTasks: () => Task[];
getOverdueTasks: () => Task[];
}
export const useTaskStore = create<TaskState>()(
persist(
(set, get) => ({
tasks: [],
isLoading: false,
addTask: (taskData) => {
const newTask: Task = {
...taskData,
id: generateId(),
createdAt: new Date(),
updatedAt: new Date(),
};
set((state) => ({ tasks: [...state.tasks, newTask] }));
},
updateTask: (id, updates) => {
set((state) => ({
tasks: state.tasks.map((task) =>
task.id === id
? { ...task, ...updates, updatedAt: new Date() }
: task
),
}));
},
deleteTask: (id) => {
set((state) => ({
tasks: state.tasks.filter((task) => task.id !== id),
}));
},
toggleComplete: (id) => {
const task = get().tasks.find((t) => t.id === id);
if (task) {
get().updateTask(id, { completed: !task.completed });
}
},
getTasksByProject: (projectId) => {
return get().tasks.filter((task) => task.projectId === projectId);
},
getTodayTasks: () => {
const today = new Date();
return get().tasks.filter((task) => {
if (!task.dueDate) return false;
return isSameDay(task.dueDate, today);
});
},
getOverdueTasks: () => {
const today = new Date();
return get().tasks.filter((task) => {
if (!task.dueDate || task.completed) return false;
return task.dueDate < today;
});
},
}),
{
name: 'task-storage',
storage: createJSONStorage(() => AsyncStorage),
}
)
);
Phase 1: Project Setup
ā±ļø Estimated time: 2-3 hours
Objectives
- Initialize Expo project with TypeScript
- Set up project structure
- Configure ESLint and Prettier
- Set up Expo Router navigation
- Create theme system with dark mode
Tasks
1.1 Initialize Project
# Create new Expo project
npx create-expo-app@latest taskflow -t tabs
cd taskflow
# Install additional dependencies
npx expo install zustand @react-native-async-storage/async-storage
npx expo install expo-haptics expo-notifications expo-local-authentication
npx expo install react-native-reanimated react-native-gesture-handler
npm install date-fns uuid
npm install -D @types/uuid
1.2 Configure Theme System
// constants/theme.ts
export const lightTheme = {
colors: {
primary: '#6366F1',
background: '#FFFFFF',
surface: '#F3F4F6',
text: '#1F2937',
textSecondary: '#6B7280',
border: '#E5E7EB',
error: '#EF4444',
success: '#10B981',
warning: '#F59E0B',
},
spacing: {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
},
borderRadius: {
sm: 4,
md: 8,
lg: 16,
full: 9999,
},
};
export const darkTheme = {
...lightTheme,
colors: {
...lightTheme.colors,
background: '#111827',
surface: '#1F2937',
text: '#F9FAFB',
textSecondary: '#9CA3AF',
border: '#374151',
},
};
1.3 Set Up Navigation Structure
// app/_layout.tsx
import { ThemeProvider } from '@/contexts/ThemeContext';
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<ThemeProvider>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="task/[id]"
options={{ presentation: 'modal', title: 'Task Details' }}
/>
<Stack.Screen
name="project/[id]"
options={{ title: 'Project' }}
/>
</Stack>
</ThemeProvider>
);
}
Deliverables
- ā Project runs without errors
- ā Tab navigation works
- ā Theme switching works
- ā Project structure matches specification
Phase 2: Core Features
ā±ļø Estimated time: 8-12 hours
Objectives
- Implement task CRUD operations
- Create project management
- Build reusable UI components
- Implement data persistence
Tasks
2.1 Build Task Components
// components/tasks/TaskItem.tsx
import { View, Text, Pressable, StyleSheet } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import Animated, {
useAnimatedStyle,
withSpring
} from 'react-native-reanimated';
import * as Haptics from 'expo-haptics';
interface TaskItemProps {
task: Task;
onToggle: () => void;
onPress: () => void;
}
export function TaskItem({ task, onToggle, onPress }: TaskItemProps) {
const handleToggle = () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
onToggle();
};
return (
<Pressable style={styles.container} onPress={onPress}>
<Pressable
style={styles.checkbox}
onPress={handleToggle}
hitSlop={8}
>
<Ionicons
name={task.completed ? 'checkmark-circle' : 'ellipse-outline'}
size={24}
color={task.completed ? '#10B981' : '#9CA3AF'}
/>
</Pressable>
<View style={styles.content}>
<Text style={[
styles.title,
task.completed && styles.completedText
]}>
{task.title}
</Text>
{task.dueDate && (
<Text style={styles.dueDate}>
{formatDueDate(task.dueDate)}
</Text>
)}
</View>
<PriorityBadge priority={task.priority} />
</Pressable>
);
}
2.2 Implement Task Form
// components/tasks/TaskForm.tsx
import { useState } from 'react';
import { View, TextInput, StyleSheet } from 'react-native';
import { DatePicker } from '@/components/ui/DatePicker';
import { PrioritySelector } from '@/components/ui/PrioritySelector';
import { ProjectPicker } from '@/components/ui/ProjectPicker';
import { Button } from '@/components/ui/Button';
interface TaskFormProps {
initialData?: Partial<Task>;
onSubmit: (data: TaskFormData) => void;
onCancel: () => void;
}
export function TaskForm({ initialData, onSubmit, onCancel }: TaskFormProps) {
const [title, setTitle] = useState(initialData?.title || '');
const [description, setDescription] = useState(initialData?.description || '');
const [priority, setPriority] = useState(initialData?.priority || 'medium');
const [dueDate, setDueDate] = useState(initialData?.dueDate);
const [projectId, setProjectId] = useState(initialData?.projectId);
const handleSubmit = () => {
if (!title.trim()) return;
onSubmit({
title: title.trim(),
description: description.trim(),
priority,
dueDate,
projectId,
});
};
return (
<View style={styles.container}>
<TextInput
style={styles.titleInput}
placeholder="Task title"
value={title}
onChangeText={setTitle}
autoFocus
/>
<TextInput
style={styles.descriptionInput}
placeholder="Description (optional)"
value={description}
onChangeText={setDescription}
multiline
/>
<PrioritySelector value={priority} onChange={setPriority} />
<DatePicker
label="Due date"
value={dueDate}
onChange={setDueDate}
/>
<ProjectPicker value={projectId} onChange={setProjectId} />
<View style={styles.buttons}>
<Button variant="secondary" onPress={onCancel}>
Cancel
</Button>
<Button onPress={handleSubmit} disabled={!title.trim()}>
{initialData ? 'Save' : 'Add Task'}
</Button>
</View>
</View>
);
}
2.3 Build Today Screen
// app/(tabs)/index.tsx
import { View, StyleSheet } from 'react-native';
import { FlashList } from '@shopify/flash-list';
import { useTaskStore } from '@/stores/taskStore';
import { TaskItem } from '@/components/tasks/TaskItem';
import { EmptyState } from '@/components/common/EmptyState';
import { SectionHeader } from '@/components/common/SectionHeader';
import { FAB } from '@/components/ui/FAB';
import { router } from 'expo-router';
export default function TodayScreen() {
const tasks = useTaskStore((s) => s.getTodayTasks());
const overdueTasks = useTaskStore((s) => s.getOverdueTasks());
const toggleComplete = useTaskStore((s) => s.toggleComplete);
const sections = [
{ title: 'Overdue', data: overdueTasks, type: 'overdue' },
{ title: 'Today', data: tasks, type: 'today' },
].filter((s) => s.data.length > 0);
const allTasks = sections.flatMap((s) => [
{ type: 'header', title: s.title, key: s.title },
...s.data.map((t) => ({ type: 'task', task: t, key: t.id })),
]);
return (
<View style={styles.container}>
<FlashList
data={allTasks}
estimatedItemSize={60}
renderItem={({ item }) => {
if (item.type === 'header') {
return <SectionHeader title={item.title} />;
}
return (
<TaskItem
task={item.task}
onToggle={() => toggleComplete(item.task.id)}
onPress={() => router.push(`/task/${item.task.id}`)}
/>
);
}}
ListEmptyComponent={
<EmptyState
icon="checkmark-done"
title="All caught up!"
message="No tasks for today"
/>
}
/>
<FAB
icon="add"
onPress={() => router.push('/task/new')}
/>
</View>
);
}
Deliverables
- ā Can create, edit, delete tasks
- ā Tasks persist after app restart
- ā Projects can be created and assigned
- ā Today view shows correct tasks
- ā Filter and search work
Phase 3: Native Features
ā±ļø Estimated time: 4-6 hours
Objectives
- Implement push notifications for reminders
- Add haptic feedback throughout
- Implement at least 2 additional native features
Tasks
3.1 Push Notifications
// services/notifications.ts
import * as Notifications from 'expo-notifications';
import { Task } from '@/types';
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
export async function scheduleTaskReminder(task: Task) {
if (!task.reminderDate) return;
await Notifications.scheduleNotificationAsync({
content: {
title: 'Task Reminder',
body: task.title,
data: { taskId: task.id },
},
trigger: {
date: task.reminderDate,
},
});
}
export async function cancelTaskReminder(taskId: string) {
const notifications = await Notifications.getAllScheduledNotificationsAsync();
const notification = notifications.find(
(n) => n.content.data?.taskId === taskId
);
if (notification) {
await Notifications.cancelScheduledNotificationAsync(notification.identifier);
}
}
export async function requestPermissions() {
const { status } = await Notifications.requestPermissionsAsync();
return status === 'granted';
}
3.2 Biometric Lock (Optional Feature)
// hooks/useBiometricAuth.ts
import * as LocalAuthentication from 'expo-local-authentication';
import { useState, useEffect } from 'react';
export function useBiometricAuth() {
const [isAvailable, setIsAvailable] = useState(false);
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
checkAvailability();
}, []);
async function checkAvailability() {
const compatible = await LocalAuthentication.hasHardwareAsync();
const enrolled = await LocalAuthentication.isEnrolledAsync();
setIsAvailable(compatible && enrolled);
}
async function authenticate() {
const result = await LocalAuthentication.authenticateAsync({
promptMessage: 'Authenticate to access TaskFlow',
fallbackLabel: 'Use passcode',
});
setIsAuthenticated(result.success);
return result.success;
}
return { isAvailable, isAuthenticated, authenticate };
}
3.3 Share Tasks
// utils/share.ts
import * as Sharing from 'expo-sharing';
import * as FileSystem from 'expo-file-system';
import { Task } from '@/types';
export async function shareTask(task: Task) {
const content = `
š ${task.title}
${task.description ? `\n${task.description}` : ''}
${task.dueDate ? `\nš
Due: ${formatDate(task.dueDate)}` : ''}
${task.priority !== 'medium' ? `\nā” Priority: ${task.priority}` : ''}
`.trim();
await Share.share({
message: content,
title: task.title,
});
}
export async function exportTasksToFile(tasks: Task[]) {
const json = JSON.stringify(tasks, null, 2);
const path = `${FileSystem.documentDirectory}taskflow-export.json`;
await FileSystem.writeAsStringAsync(path, json);
if (await Sharing.isAvailableAsync()) {
await Sharing.shareAsync(path);
}
}
Native Features Checklist
- ā Push notifications for task reminders
- ā Haptic feedback on interactions
- Choose 2+ from:
- Biometric authentication
- Share functionality
- File export/import
- Camera for task attachments
- Location-based reminders
Phase 4: Polish & Animation
ā±ļø Estimated time: 4-6 hours
Objectives
- Add meaningful animations throughout
- Implement gesture-based interactions
- Polish UI details and transitions
Required Animations
4.1 Swipe to Complete/Delete
// components/tasks/SwipeableTaskItem.tsx
import Animated, {
useAnimatedStyle,
useSharedValue,
withSpring,
runOnJS,
} from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
export function SwipeableTaskItem({ task, onComplete, onDelete }) {
const translateX = useSharedValue(0);
const SWIPE_THRESHOLD = 100;
const gesture = Gesture.Pan()
.onUpdate((e) => {
translateX.value = e.translationX;
})
.onEnd((e) => {
if (e.translationX > SWIPE_THRESHOLD) {
// Swipe right = complete
translateX.value = withSpring(200, {}, () => {
runOnJS(onComplete)();
});
} else if (e.translationX < -SWIPE_THRESHOLD) {
// Swipe left = delete
translateX.value = withSpring(-200, {}, () => {
runOnJS(onDelete)();
});
} else {
translateX.value = withSpring(0);
}
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}));
return (
<GestureDetector gesture={gesture}>
<Animated.View style={animatedStyle}>
<TaskItem task={task} />
</Animated.View>
</GestureDetector>
);
}
4.2 Layout Animations
// Use Reanimated Layout Animations
import Animated, {
FadeIn,
FadeOut,
Layout
} from 'react-native-reanimated';
// In your list:
<Animated.View
entering={FadeIn.duration(300)}
exiting={FadeOut.duration(200)}
layout={Layout.springify()}
>
<TaskItem task={task} />
</Animated.View>
4.3 Micro-interactions
// Checkbox animation
const scale = useSharedValue(1);
const handlePress = () => {
scale.value = withSequence(
withSpring(0.8),
withSpring(1.2),
withSpring(1)
);
onToggle();
};
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
// FAB press animation
const fabScale = useSharedValue(1);
const fabGesture = Gesture.Tap()
.onBegin(() => {
fabScale.value = withSpring(0.9);
})
.onFinalize(() => {
fabScale.value = withSpring(1);
});
Animation Checklist
- ā Swipe gestures on task items
- ā List item enter/exit animations
- ā Checkbox completion animation
- ā Screen transitions
- ā Loading states with skeletons
- ā Pull-to-refresh animation
Phase 5: Testing
ā±ļø Estimated time: 4-6 hours
Objectives
- Write unit tests for stores and utilities
- Write component tests for key components
- Achieve >60% code coverage
Test Examples
Store Tests
// __tests__/stores/taskStore.test.ts
import { useTaskStore } from '@/stores/taskStore';
describe('TaskStore', () => {
beforeEach(() => {
useTaskStore.setState({ tasks: [] });
});
it('adds a task', () => {
const { addTask, tasks } = useTaskStore.getState();
addTask({
title: 'Test Task',
priority: 'medium',
completed: false,
tags: [],
subtasks: [],
});
expect(useTaskStore.getState().tasks).toHaveLength(1);
expect(useTaskStore.getState().tasks[0].title).toBe('Test Task');
});
it('toggles task completion', () => {
useTaskStore.setState({
tasks: [{
id: '1',
title: 'Test',
completed: false,
priority: 'medium',
tags: [],
subtasks: [],
createdAt: new Date(),
updatedAt: new Date(),
}],
});
useTaskStore.getState().toggleComplete('1');
expect(useTaskStore.getState().tasks[0].completed).toBe(true);
});
it('filters today tasks correctly', () => {
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
useTaskStore.setState({
tasks: [
{ id: '1', title: 'Today', dueDate: today, ...baseTask },
{ id: '2', title: 'Tomorrow', dueDate: tomorrow, ...baseTask },
],
});
const todayTasks = useTaskStore.getState().getTodayTasks();
expect(todayTasks).toHaveLength(1);
expect(todayTasks[0].title).toBe('Today');
});
});
Component Tests
// __tests__/components/TaskItem.test.tsx
import { render, fireEvent } from '@testing-library/react-native';
import { TaskItem } from '@/components/tasks/TaskItem';
describe('TaskItem', () => {
const mockTask = {
id: '1',
title: 'Test Task',
completed: false,
priority: 'high' as const,
};
it('renders task title', () => {
const { getByText } = render(
<TaskItem task={mockTask} onToggle={jest.fn()} onPress={jest.fn()} />
);
expect(getByText('Test Task')).toBeTruthy();
});
it('calls onToggle when checkbox pressed', () => {
const onToggle = jest.fn();
const { getByTestId } = render(
<TaskItem task={mockTask} onToggle={onToggle} onPress={jest.fn()} />
);
fireEvent.press(getByTestId('checkbox'));
expect(onToggle).toHaveBeenCalled();
});
it('shows strikethrough when completed', () => {
const completedTask = { ...mockTask, completed: true };
const { getByText } = render(
<TaskItem task={completedTask} onToggle={jest.fn()} onPress={jest.fn()} />
);
const title = getByText('Test Task');
expect(title.props.style).toContainEqual(
expect.objectContaining({ textDecorationLine: 'line-through' })
);
});
});
Coverage Requirements
- ā Stores: >80% coverage
- ā Utilities: >80% coverage
- ā Components: >50% coverage
- ā Overall: >60% coverage
Phase 6: Production & Deployment
ā±ļø Estimated time: 4-6 hours
Objectives
- Configure app for production
- Set up EAS Build and Submit
- Configure crash reporting
- Prepare store listings
Production Checklist
6.1 App Configuration
// app.config.ts
export default {
expo: {
name: 'TaskFlow',
slug: 'taskflow',
version: '1.0.0',
orientation: 'portrait',
icon: './assets/icon.png',
splash: {
image: './assets/splash.png',
resizeMode: 'contain',
backgroundColor: '#6366F1',
},
ios: {
bundleIdentifier: 'com.yourname.taskflow',
buildNumber: '1',
supportsTablet: true,
},
android: {
package: 'com.yourname.taskflow',
versionCode: 1,
adaptiveIcon: {
foregroundImage: './assets/adaptive-icon.png',
backgroundColor: '#6366F1',
},
},
plugins: [
'expo-router',
[
'expo-notifications',
{
icon: './assets/notification-icon.png',
color: '#6366F1',
},
],
'@sentry/react-native/expo',
],
extra: {
eas: {
projectId: 'your-project-id',
},
},
},
};
6.2 EAS Configuration
// eas.json
{
"cli": {
"version": ">= 7.0.0",
"appVersionSource": "remote"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"channel": "development"
},
"preview": {
"distribution": "internal",
"channel": "preview"
},
"production": {
"channel": "production",
"autoIncrement": true
}
},
"submit": {
"production": {
"ios": {
"appleId": "your@email.com",
"ascAppId": "your-app-id"
},
"android": {
"serviceAccountKeyPath": "./google-play-key.json",
"track": "internal"
}
}
}
}
6.3 Sentry Integration
// App.tsx
import * as Sentry from '@sentry/react-native';
Sentry.init({
dsn: process.env.EXPO_PUBLIC_SENTRY_DSN,
enabled: !__DEV__,
tracesSampleRate: 0.2,
});
export default Sentry.wrap(App);
Deployment Steps
- Run final tests:
npm test - Build preview:
eas build --profile preview - Test on real devices
- Build production:
eas build --profile production - Submit to stores:
eas submit --platform all
Evaluation Criteria
| Category | Weight | Criteria |
|---|---|---|
| Functionality | 25% | All required features work correctly |
| Code Quality | 20% | Clean, typed, well-organized code |
| UI/UX | 20% | Polished, intuitive, platform-appropriate |
| Animations | 10% | Smooth, meaningful animations |
| Testing | 15% | >60% coverage, quality tests |
| Production Ready | 10% | Proper config, error handling, monitoring |
Grading Scale
A (90-100%)
Exceeds expectations with bonus features
B (80-89%)
Meets all requirements with polish
C (70-79%)
Meets core requirements
D (60-69%)
Missing some requirements
Resources & Tips
Helpful Resources
Pro Tips
š” Development Tips
- Start with the data model and store before UI
- Build and test features incrementally
- Use the Expo Go app for rapid development
- Test on real devices early and often
- Write tests as you build, not after
ā ļø Common Pitfalls
- Don't over-engineer ā start simple
- Don't forget dark mode styling
- Don't skip error handling
- Don't ignore TypeScript errors
- Don't wait until the end to test on devices
Timeline Suggestion
| Week | Focus |
|---|---|
| Week 1 | Phase 1 + Phase 2 (Setup + Core Features) |
| Week 2 | Phase 3 + Phase 4 (Native + Polish) |
| Week 3 | Phase 5 + Phase 6 (Testing + Deployment) |
š Good Luck!
This capstone project is your opportunity to demonstrate everything you've learned. Take your time, focus on quality, and build something you're proud of. You've got this!