Apply your knowledge to build something amazing!
FitTrack is a health and wellness dashboard that empowers users to take control of their fitness journey through data-driven insights. By integrating with Health Connect (Android) and HealthKit (iOS), the app aggregates fitness data from multiple sources including step counters, workout apps, and sleep trackers. Users can set personalized goals, visualize progress through interactive charts, and earn achievements as they reach milestones.
This project demonstrates your ability to work with sensitive health data while following platform-specific privacy guidelines. You'll implement data visualization using Victory Charts, create a goal-setting system with local notifications, and build an engaging achievement system that motivates users. The result is a production-ready wellness app that users will want to check daily to track their health journey.
Unlike fitness apps that require manual logging, FitTrack automatically syncs data from existing health platforms, providing a seamless experience. You can extend this foundation to include nutrition tracking, social challenges, or integration with wearable devices.
By completing this project, you will:
src/
├── components/
│ ├── StatsCard.js
│ ├── GoalCard.js
│ ├── ProgressRing.js
│ ├── TrendChart.js
│ ├── AchievementBadge.js
│ └── HealthDataPermissions.js
├── screens/
│ ├── DashboardScreen.js
│ ├── GoalsScreen.js
│ ├── ChartsScreen.js
│ ├── AchievementsScreen.js
│ └── SettingsScreen.js
├── services/
│ ├── healthService.js
│ ├── goalsService.js
│ ├── achievementsService.js
│ └── notificationService.js
├── hooks/
│ ├── useHealthData.js
│ ├── useGoals.js
│ └── useAchievements.js
├── utils/
│ ├── calculations.js
│ ├── dateHelpers.js
│ └── constants.js
└── navigation/
└── AppNavigator.js
On app launch, FitTrack requests health data permissions and checks authorization status. Once granted, the app fetches today's health data and displays it on the dashboard. Users can navigate to goals screen to set targets, which are saved locally. The achievement system monitors progress and unlocks badges when criteria are met. Notifications are scheduled daily to remind users to check their progress. Charts aggregate historical data to show trends over time.
Goal: Set up health data access and dashboard structure
Initialize project and install health and chart dependencies.
npx create-expo-app FitTrack
cd FitTrack
npx expo install react-native-health-connect expo-notifications
npm install victory-native react-native-svg
npx expo install @react-native-async-storage/async-storage
npm install @react-navigation/native @react-navigation/bottom-tabs
npx expo install react-native-reanimated
Configure app.json with necessary permissions:
{
"expo": {
"name": "FitTrack",
"slug": "fittrack",
"plugins": [
[
"expo-notifications",
{
"icon": "./assets/notification-icon.png",
"color": "#4CAF50"
}
]
],
"android": {
"package": "com.yourcompany.fittrack",
"permissions": [
"android.permission.ACTIVITY_RECOGNITION",
"android.permission.health.READ_STEPS",
"android.permission.health.READ_DISTANCE",
"android.permission.health.READ_CALORIES_BURNED"
]
},
"ios": {
"infoPlist": {
"NSHealthShareUsageDescription": "FitTrack needs access to your health data to track your fitness progress.",
"NSHealthUpdateUsageDescription": "FitTrack needs to update your health data."
}
}
}
}
Create the health service to manage data access.
// src/services/healthService.js
import HealthConnect from 'react-native-health-connect';
import { Platform } from 'react-native';
const HEALTH_DATA_TYPES = {
steps: 'Steps',
calories: 'TotalCaloriesBurned',
activeMinutes: 'ActiveCaloriesBurned',
sleep: 'SleepSession',
heartRate: 'HeartRate',
};
export const requestHealthPermissions = async () => {
try {
if (Platform.OS === 'android') {
const permissions = [
{ accessType: 'read', recordType: 'Steps' },
{ accessType: 'read', recordType: 'TotalCaloriesBurned' },
{ accessType: 'read', recordType: 'ActiveCaloriesBurned' },
{ accessType: 'read', recordType: 'SleepSession' },
];
const granted = await HealthConnect.requestPermission(permissions);
return granted;
}
// iOS HealthKit permissions would go here
return true;
} catch (error) {
console.error('Permission request error:', error);
return false;
}
};
export const getStepsToday = async () => {
try {
const today = new Date();
const startOfDay = new Date(today.setHours(0, 0, 0, 0));
const endOfDay = new Date(today.setHours(23, 59, 59, 999));
const result = await HealthConnect.readRecords('Steps', {
timeRangeFilter: {
operator: 'between',
startTime: startOfDay.toISOString(),
endTime: endOfDay.toISOString(),
},
});
const totalSteps = result.records.reduce((sum, record) => sum + (record.count || 0), 0);
return totalSteps;
} catch (error) {
console.error('Steps fetch error:', error);
return 0;
}
};
export const getCaloriesToday = async () => {
try {
const today = new Date();
const startOfDay = new Date(today.setHours(0, 0, 0, 0));
const endOfDay = new Date(today.setHours(23, 59, 59, 999));
const result = await HealthConnect.readRecords('TotalCaloriesBurned', {
timeRangeFilter: {
operator: 'between',
startTime: startOfDay.toISOString(),
endTime: endOfDay.toISOString(),
},
});
const totalCalories = result.records.reduce(
(sum, record) => sum + (record.energy?.inKilocalories || 0),
0
);
return Math.round(totalCalories);
} catch (error) {
console.error('Calories fetch error:', error);
return 0;
}
};
export const getActiveMinutesToday = async () => {
try {
const today = new Date();
const startOfDay = new Date(today.setHours(0, 0, 0, 0));
const endOfDay = new Date(today.setHours(23, 59, 59, 999));
const result = await HealthConnect.readRecords('ActiveCaloriesBurned', {
timeRangeFilter: {
operator: 'between',
startTime: startOfDay.toISOString(),
endTime: endOfDay.toISOString(),
},
});
// Estimate active minutes from calorie burn rate
const activeCalories = result.records.reduce(
(sum, record) => sum + (record.energy?.inKilocalories || 0),
0
);
const estimatedMinutes = Math.round(activeCalories / 6); // Rough estimate: 6 cal/min
return estimatedMinutes;
} catch (error) {
console.error('Active minutes fetch error:', error);
return 0;
}
};
export const getWeeklySteps = async () => {
try {
const today = new Date();
const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
const result = await HealthConnect.readRecords('Steps', {
timeRangeFilter: {
operator: 'between',
startTime: weekAgo.toISOString(),
endTime: today.toISOString(),
},
});
// Group by day
const dailySteps = {};
result.records.forEach(record => {
const date = new Date(record.startTime).toDateString();
dailySteps[date] = (dailySteps[date] || 0) + (record.count || 0);
});
return Object.entries(dailySteps).map(([date, steps]) => ({
date,
steps,
}));
} catch (error) {
console.error('Weekly steps fetch error:', error);
return [];
}
};
Create the main dashboard displaying today's health stats.
// src/screens/DashboardScreen.js
import React, { useState, useEffect } from 'react';
import { View, Text, ScrollView, RefreshControl, StyleSheet, Alert } from 'react-native';
import {
getStepsToday,
getCaloriesToday,
getActiveMinutesToday,
requestHealthPermissions,
} from '../services/healthService';
import StatsCard from '../components/StatsCard';
import ProgressRing from '../components/ProgressRing';
export default function DashboardScreen() {
const [healthData, setHealthData] = useState({
steps: 0,
calories: 0,
activeMinutes: 0,
});
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [hasPermission, setHasPermission] = useState(false);
useEffect(() => {
initializeHealth();
}, []);
const initializeHealth = async () => {
const granted = await requestHealthPermissions();
setHasPermission(granted);
if (granted) {
await fetchHealthData();
} else {
Alert.alert(
'Health Access Required',
'FitTrack needs access to your health data to track your fitness progress.'
);
}
setLoading(false);
};
const fetchHealthData = async () => {
try {
const [steps, calories, activeMinutes] = await Promise.all([
getStepsToday(),
getCaloriesToday(),
getActiveMinutesToday(),
]);
setHealthData({
steps,
calories,
activeMinutes,
});
} catch (error) {
console.error('Health data fetch error:', error);
Alert.alert('Error', 'Failed to fetch health data. Please try again.');
}
};
const onRefresh = async () => {
setRefreshing(true);
await fetchHealthData();
setRefreshing(false);
};
if (!hasPermission) {
return (
<View style={styles.container}>
<Text style={styles.noPermissionText}>
Health data access not granted
</Text>
</View>
);
}
return (
<ScrollView
style={styles.container}
contentContainerStyle={styles.content}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
>
<View style={styles.header}>
<Text style={styles.greeting}>Hello! 👋</Text>
<Text style={styles.date}>{new Date().toLocaleDateString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric'
})}</Text>
</View>
<View style={styles.mainStats}>
<ProgressRing
value={healthData.steps}
maxValue={10000}
label="Steps"
color="#4CAF50"
size={180}
/>
</View>
<View style={styles.statsGrid}>
<StatsCard
icon="🔥"
title="Calories"
value={healthData.calories}
unit="kcal"
goal={2000}
color="#FF5722"
/>
<StatsCard
icon="⏱️"
title="Active"
value={healthData.activeMinutes}
unit="min"
goal={30}
color="#2196F3"
/>
</View>
<View style={styles.quickStats}>
<Text style={styles.sectionTitle}>This Week</Text>
<Text style={styles.quickStatsText}>
🏃 Average: {Math.round(healthData.steps / 7)} steps/day
</Text>
<Text style={styles.quickStatsText}>
🔥 Total: {healthData.calories * 7} calories burned
</Text>
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
content: {
padding: 20,
},
header: {
marginBottom: 30,
},
greeting: {
fontSize: 32,
fontWeight: 'bold',
color: '#333',
},
date: {
fontSize: 16,
color: '#666',
marginTop: 5,
},
mainStats: {
alignItems: 'center',
marginBottom: 30,
},
statsGrid: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 30,
},
quickStats: {
backgroundColor: 'white',
padding: 20,
borderRadius: 12,
},
sectionTitle: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 15,
},
quickStatsText: {
fontSize: 16,
color: '#666',
marginBottom: 10,
},
noPermissionText: {
fontSize: 18,
color: '#666',
textAlign: 'center',
},
});
Goal: Implement goals system and data visualization
Create reusable components for displaying health metrics.
// src/components/StatsCard.js
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
export default function StatsCard({ icon, title, value, unit, goal, color }) {
const progress = goal ? (value / goal) * 100 : 0;
const progressCapped = Math.min(progress, 100);
return (
<View style={styles.container}>
<Text style={styles.icon}>{icon}</Text>
<Text style={styles.title}>{title}</Text>
<Text style={styles.value}>
{value.toLocaleString()}
<Text style={styles.unit}> {unit}</Text>
</Text>
{goal && (
<>
<View style={styles.progressBar}>
<View
style={[
styles.progressFill,
{ width: `${progressCapped}%`, backgroundColor: color },
]}
/>
</View>
<Text style={styles.goalText}>
Goal: {goal.toLocaleString()} {unit}
</Text>
</>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'white',
padding: 15,
borderRadius: 12,
marginHorizontal: 5,
},
icon: {
fontSize: 32,
marginBottom: 10,
},
title: {
fontSize: 14,
color: '#666',
marginBottom: 5,
},
value: {
fontSize: 24,
fontWeight: 'bold',
color: '#333',
},
unit: {
fontSize: 14,
fontWeight: 'normal',
color: '#999',
},
progressBar: {
height: 6,
backgroundColor: '#e0e0e0',
borderRadius: 3,
marginTop: 10,
overflow: 'hidden',
},
progressFill: {
height: '100%',
borderRadius: 3,
},
goalText: {
fontSize: 11,
color: '#999',
marginTop: 5,
},
});
// src/components/ProgressRing.js
import React, { useEffect } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedProps,
withTiming,
} from 'react-native-reanimated';
import { Circle, Svg } from 'react-native-svg';
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
export default function ProgressRing({ value, maxValue, label, color, size = 200 }) {
const progress = useSharedValue(0);
const radius = (size - 20) / 2;
const circumference = 2 * Math.PI * radius;
useEffect(() => {
progress.value = withTiming((value / maxValue) * 100, { duration: 1000 });
}, [value, maxValue]);
const animatedProps = useAnimatedProps(() => {
const strokeDashoffset = circumference - (progress.value / 100) * circumference;
return {
strokeDashoffset,
};
});
const percentage = Math.round((value / maxValue) * 100);
return (
<View style={styles.container}>
<Svg width={size} height={size}>
<Circle
cx={size / 2}
cy={size / 2}
r={radius}
stroke="#e0e0e0"
strokeWidth={15}
fill="none"
/>
<AnimatedCircle
cx={size / 2}
cy={size / 2}
r={radius}
stroke={color}
strokeWidth={15}
fill="none"
strokeDasharray={circumference}
strokeDashoffset={circumference}
animatedProps={animatedProps}
strokeLinecap="round"
rotation="-90"
origin={`${size / 2}, ${size / 2}`}
/>
</Svg>
<View style={styles.content}>
<Text style={styles.value}>{value.toLocaleString()}</Text>
<Text style={styles.label}>{label}</Text>
<Text style={styles.percentage}>{percentage}%</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
justifyContent: 'center',
},
content: {
position: 'absolute',
alignItems: 'center',
},
value: {
fontSize: 40,
fontWeight: 'bold',
color: '#333',
},
label: {
fontSize: 16,
color: '#666',
marginTop: 5,
},
percentage: {
fontSize: 14,
color: '#999',
marginTop: 2,
},
});
Implement goal setting and tracking functionality.
// src/services/goalsService.js
import AsyncStorage from '@react-native-async-storage/async-storage';
const GOALS_KEY = '@fittrack_goals';
const DEFAULT_GOALS = {
steps: 10000,
calories: 2000,
activeMinutes: 30,
weight: 70,
};
export const getGoals = async () => {
try {
const goalsJson = await AsyncStorage.getItem(GOALS_KEY);
return goalsJson ? JSON.parse(goalsJson) : DEFAULT_GOALS;
} catch (error) {
console.error('Load goals error:', error);
return DEFAULT_GOALS;
}
};
export const saveGoals = async (goals) => {
try {
await AsyncStorage.setItem(GOALS_KEY, JSON.stringify(goals));
} catch (error) {
console.error('Save goals error:', error);
throw error;
}
};
export const calculateProgress = (current, goal) => {
if (!goal || goal === 0) return 0;
return Math.round((current / goal) * 100);
};
export const isGoalMet = (current, goal) => {
return current >= goal;
};
// src/screens/GoalsScreen.js
import React, { useState, useEffect } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, ScrollView, Alert } from 'react-native';
import { getGoals, saveGoals } from '../services/goalsService';
export default function GoalsScreen() {
const [goals, setGoals] = useState({
steps: 10000,
calories: 2000,
activeMinutes: 30,
});
const [editMode, setEditMode] = useState(false);
useEffect(() => {
loadGoals();
}, []);
const loadGoals = async () => {
const savedGoals = await getGoals();
setGoals(savedGoals);
};
const handleSave = async () => {
try {
await saveGoals(goals);
setEditMode(false);
Alert.alert('Success', 'Goals updated successfully!');
} catch (error) {
Alert.alert('Error', 'Failed to save goals. Please try again.');
}
};
const updateGoal = (key, value) => {
const numValue = parseInt(value, 10) || 0;
setGoals(prev => ({ ...prev, [key]: numValue }));
};
return (
<ScrollView style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>Your Goals</Text>
<TouchableOpacity
onPress={() => editMode ? handleSave() : setEditMode(true)}
>
<Text style={styles.editButton}>{editMode ? 'Save' : 'Edit'}</Text>
</TouchableOpacity>
</View>
<View style={styles.goalCard}>
<Text style={styles.goalIcon}>👟</Text>
<Text style={styles.goalTitle}>Daily Steps</Text>
{editMode ? (
<TextInput
style={styles.input}
value={goals.steps.toString()}
onChangeText={(value) => updateGoal('steps', value)}
keyboardType="number-pad"
/>
) : (
<Text style={styles.goalValue}>{goals.steps.toLocaleString()} steps</Text>
)}
<Text style={styles.goalDescription}>
Walking 10,000 steps burns approximately 400-500 calories
</Text>
</View>
<View style={styles.goalCard}>
<Text style={styles.goalIcon}>🔥</Text>
<Text style={styles.goalTitle}>Daily Calories</Text>
{editMode ? (
<TextInput
style={styles.input}
value={goals.calories.toString()}
onChangeText={(value) => updateGoal('calories', value)}
keyboardType="number-pad"
/>
) : (
<Text style={styles.goalValue}>{goals.calories.toLocaleString()} kcal</Text>
)}
<Text style={styles.goalDescription}>
Recommended daily calorie burn for weight maintenance
</Text>
</View>
<View style={styles.goalCard}>
<Text style={styles.goalIcon}>⏱️</Text>
<Text style={styles.goalTitle}>Active Minutes</Text>
{editMode ? (
<TextInput
style={styles.input}
value={goals.activeMinutes.toString()}
onChangeText={(value) => updateGoal('activeMinutes', value)}
keyboardType="number-pad"
/>
) : (
<Text style={styles.goalValue}>{goals.activeMinutes} minutes</Text>
)}
<Text style={styles.goalDescription}>
WHO recommends at least 150 minutes of activity per week
</Text>
</View>
{editMode && (
<TouchableOpacity
style={styles.cancelButton}
onPress={() => {
setEditMode(false);
loadGoals();
}}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
)}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 20,
},
title: {
fontSize: 28,
fontWeight: 'bold',
},
editButton: {
fontSize: 18,
color: '#4CAF50',
fontWeight: '600',
},
goalCard: {
backgroundColor: 'white',
padding: 20,
marginHorizontal: 20,
marginBottom: 15,
borderRadius: 12,
},
goalIcon: {
fontSize: 36,
marginBottom: 10,
},
goalTitle: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 10,
},
goalValue: {
fontSize: 32,
fontWeight: 'bold',
color: '#4CAF50',
marginBottom: 10,
},
input: {
fontSize: 32,
fontWeight: 'bold',
color: '#4CAF50',
borderBottomWidth: 2,
borderBottomColor: '#4CAF50',
marginBottom: 10,
paddingVertical: 5,
},
goalDescription: {
fontSize: 14,
color: '#666',
lineHeight: 20,
},
cancelButton: {
backgroundColor: '#f44336',
padding: 15,
marginHorizontal: 20,
marginBottom: 30,
borderRadius: 8,
alignItems: 'center',
},
cancelButtonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
});
Add data visualization with Victory Charts.
// src/screens/ChartsScreen.js
import React, { useState, useEffect } from 'react';
import { View, Text, ScrollView, StyleSheet, Dimensions } from 'react-native';
import { VictoryLine, VictoryBar, VictoryChart, VictoryAxis, VictoryTheme } from 'victory-native';
import { getWeeklySteps } from '../services/healthService';
const screenWidth = Dimensions.get('window').width;
export default function ChartsScreen() {
const [weeklyData, setWeeklyData] = useState([]);
useEffect(() => {
loadWeeklyData();
}, []);
const loadWeeklyData = async () => {
const data = await getWeeklySteps();
setWeeklyData(data.slice(-7)); // Last 7 days
};
const chartData = weeklyData.map((item, index) => ({
x: index + 1,
y: item.steps,
label: new Date(item.date).toLocaleDateString('en-US', { weekday: 'short' }),
}));
return (
<ScrollView style={styles.container}>
<View style={styles.section}>
<Text style={styles.sectionTitle}>7-Day Step Trend</Text>
<VictoryChart
width={screenWidth - 40}
height={250}
theme={VictoryTheme.material}
>
<VictoryAxis
tickFormat={(t) => chartData[t - 1]?.label || ''}
style={{
tickLabels: { fontSize: 12, padding: 5 },
}}
/>
<VictoryAxis
dependentAxis
tickFormat={(y) => `${y / 1000}K`}
style={{
tickLabels: { fontSize: 12, padding: 5 },
}}
/>
<VictoryLine
data={chartData}
style={{
data: { stroke: '#4CAF50', strokeWidth: 3 },
}}
animate={{
duration: 1000,
onLoad: { duration: 500 },
}}
/>
</VictoryChart>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Weekly Comparison</Text>
<VictoryChart
width={screenWidth - 40}
height={250}
theme={VictoryTheme.material}
domainPadding={{ x: 30 }}
>
<VictoryAxis
tickFormat={(t) => chartData[t - 1]?.label || ''}
style={{
tickLabels: { fontSize: 12, padding: 5 },
}}
/>
<VictoryAxis
dependentAxis
tickFormat={(y) => `${y / 1000}K`}
style={{
tickLabels: { fontSize: 12, padding: 5 },
}}
/>
<VictoryBar
data={chartData}
style={{
data: { fill: '#2196F3' },
}}
animate={{
duration: 1000,
onLoad: { duration: 500 },
}}
/>
</VictoryChart>
</View>
<View style={styles.stats}>
<View style={styles.statItem}>
<Text style={styles.statLabel}>Average</Text>
<Text style={styles.statValue}>
{Math.round(
chartData.reduce((sum, d) => sum + d.y, 0) / chartData.length
).toLocaleString()}
</Text>
<Text style={styles.statUnit}>steps/day</Text>
</View>
<View style={styles.statItem}>
<Text style={styles.statLabel}>Total</Text>
<Text style={styles.statValue}>
{chartData.reduce((sum, d) => sum + d.y, 0).toLocaleString()}
</Text>
<Text style={styles.statUnit}>steps</Text>
</View>
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
section: {
backgroundColor: 'white',
padding: 20,
marginBottom: 20,
},
sectionTitle: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 15,
},
stats: {
flexDirection: 'row',
backgroundColor: 'white',
padding: 20,
marginBottom: 20,
},
statItem: {
flex: 1,
alignItems: 'center',
},
statLabel: {
fontSize: 14,
color: '#666',
marginBottom: 5,
},
statValue: {
fontSize: 28,
fontWeight: 'bold',
color: '#333',
},
statUnit: {
fontSize: 12,
color: '#999',
marginTop: 2,
},
});
Goal: Add achievements and notifications
Create achievement tracking and unlock animations.
// src/services/achievementsService.js
import AsyncStorage from '@react-native-async-storage/async-storage';
const ACHIEVEMENTS_KEY = '@fittrack_achievements';
export const ACHIEVEMENTS = [
{
id: 'first_steps',
title: 'First Steps',
description: 'Log your first 1,000 steps',
icon: '👣',
requirement: { type: 'steps', value: 1000 },
},
{
id: 'walker',
title: 'Daily Walker',
description: 'Reach 10,000 steps in a day',
icon: '🚶',
requirement: { type: 'steps', value: 10000 },
},
{
id: 'marathon',
title: 'Marathon',
description: 'Walk 50,000 steps in a week',
icon: '🏃',
requirement: { type: 'weekly_steps', value: 50000 },
},
{
id: 'consistent',
title: 'Consistency King',
description: 'Meet your goal 7 days in a row',
icon: '👑',
requirement: { type: 'streak', value: 7 },
},
{
id: 'calorie_crusher',
title: 'Calorie Crusher',
description: 'Burn 3,000 calories in a day',
icon: '🔥',
requirement: { type: 'calories', value: 3000 },
},
];
export const getUnlockedAchievements = async () => {
try {
const achievementsJson = await AsyncStorage.getItem(ACHIEVEMENTS_KEY);
return achievementsJson ? JSON.parse(achievementsJson) : [];
} catch (error) {
console.error('Load achievements error:', error);
return [];
}
};
export const unlockAchievement = async (achievementId) => {
try {
const unlocked = await getUnlockedAchievements();
if (!unlocked.includes(achievementId)) {
unlocked.push(achievementId);
await AsyncStorage.setItem(ACHIEVEMENTS_KEY, JSON.stringify(unlocked));
return true; // New unlock
}
return false; // Already unlocked
} catch (error) {
console.error('Unlock achievement error:', error);
return false;
}
};
export const checkAchievements = async (healthData) => {
const newUnlocks = [];
for (const achievement of ACHIEVEMENTS) {
const { type, value } = achievement.requirement;
let met = false;
if (type === 'steps' && healthData.steps >= value) met = true;
if (type === 'calories' && healthData.calories >= value) met = true;
if (type === 'streak' && healthData.streak >= value) met = true;
if (type === 'weekly_steps' && healthData.weeklySteps >= value) met = true;
if (met) {
const isNew = await unlockAchievement(achievement.id);
if (isNew) newUnlocks.push(achievement);
}
}
return newUnlocks;
};
Implement daily reminders and achievement notifications.
// src/services/notificationService.js
import * as Notifications from 'expo-notifications';
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
export const requestNotificationPermissions = async () => {
const { status } = await Notifications.requestPermissionsAsync();
return status === 'granted';
};
export const scheduleDailyReminder = async () => {
await Notifications.cancelAllScheduledNotificationsAsync();
await Notifications.scheduleNotificationAsync({
content: {
title: 'Time to Check In! 📊',
body: 'See how close you are to your daily fitness goals.',
},
trigger: {
hour: 20,
minute: 0,
repeats: true,
},
});
};
export const sendAchievementNotification = async (achievement) => {
await Notifications.scheduleNotificationAsync({
content: {
title: `Achievement Unlocked! ${achievement.icon}`,
body: `${achievement.title}: ${achievement.description}`,
},
trigger: null, // Send immediately
});
};
Goal: Final testing, optimization, and deployment
💡 Tip: Test health data integration thoroughly on physical devices. Use Health Connect's test data on Android or HealthKit simulator data on iOS during development.
| Criteria | Points | Description |
|---|---|---|
| Functionality | 40 | Health data syncs correctly, goals work, charts display, achievements unlock |
| Code Quality | 25 | Efficient data fetching, proper error handling, clean service architecture |
| Design/UX | 20 | Attractive dashboard, clear data visualization, smooth animations |
| Documentation | 10 | Setup guide, health permissions explanation, data privacy notes |
| Creativity | 5 | Custom achievements, unique features, social sharing, or wellness tips |
| Total | 100 |
| Week | Milestone | Deliverable |
|---|---|---|
| 1 | Phase 1-2 Complete | Dashboard with health data, goals system, basic charts working |
| 2 | Phase 3-4 Complete | Full app with achievements, notifications, deployed to Expo |
For students who finish early:
eas buildAfter completing this project: