surge/mobile/app/screens/ProgressScreen.tsx

345 lines
10 KiB
TypeScript

import React, { useEffect, useState } from 'react'
import { View, Text, StyleSheet, FlatList, TouchableOpacity, ActivityIndicator } from 'react-native'
import { withObservables } from '@nozbe/watermelondb/react'
import { Observable } from 'rxjs'
import { map, switchMap } from 'rxjs/operators'
import { Q } from '@nozbe/watermelondb'
import { database } from '../../src/db/index'
import UserChallenge from '../../src/db/models/UserChallenge'
import DailyProgress from '../../src/db/models/DailyProgress'
import TaskCompletion from '../../src/db/models/TaskCompletion'
import { getActiveUserChallenge, getOrCreateDailyProgress, getOrCreateTaskCompletions, toggleTaskCompletion } from '../../src/utils/progress'
import { Colors } from '../../constants/theme'
import { IconSymbol } from '../../components/ui/icon-symbol'
interface TaskItemProps {
taskCompletion: TaskCompletion
onToggle: (task: TaskCompletion, isCompleted: boolean) => void
}
const TaskItem = withObservables(['taskCompletion'], ({ taskCompletion }) => ({
taskCompletion,
requirement: taskCompletion.requirement,
}))(({ taskCompletion, requirement, onToggle }: TaskItemProps & { requirement: any }) => {
const isCompleted = taskCompletion.completedAt > 0
return (
<TouchableOpacity
style={styles.taskItem}
onPress={() => onToggle(taskCompletion, !isCompleted)}
>
<View style={[styles.checkbox, isCompleted && styles.checkboxChecked]}>
{isCompleted && <IconSymbol name="checkmark" size={16} color="white" />}
</View>
<View style={styles.taskContent}>
<Text style={[styles.taskTitle, isCompleted && styles.taskTitleCompleted]}>
{requirement.title}
</Text>
{requirement.description && (
<Text style={styles.taskDescription}>{requirement.description}</Text>
)}
</View>
</TouchableOpacity>
)
})
interface ProgressScreenProps {
userChallenge: UserChallenge | null
dailyProgress: DailyProgress | null
taskCompletions: TaskCompletion[]
}
const ProgressScreen = ({ userChallenge, dailyProgress, taskCompletions }: ProgressScreenProps) => {
const [error, setError] = useState<string | null>(null)
console.log('ProgressScreen: render', {
hasUserChallenge: !!userChallenge,
hasDailyProgress: !!dailyProgress,
taskCompletionsCount: taskCompletions.length,
error
})
useEffect(() => {
const ensureProgress = async () => {
if (userChallenge) {
try {
setError(null)
console.log('ProgressScreen: ensuring progress for challenge', userChallenge.id)
const progress = await getOrCreateDailyProgress(userChallenge)
console.log('ProgressScreen: daily progress obtained', progress.id)
await getOrCreateTaskCompletions(progress)
console.log('ProgressScreen: task completions ensured')
} catch (e: any) {
console.error('Error ensuring daily progress:', e)
setError(e.message || 'Failed to load progress')
}
}
}
ensureProgress()
}, [userChallenge])
if (!userChallenge) {
return (
<View style={styles.centered}>
<Text style={styles.emptyText}>No active challenge found.</Text>
<Text style={styles.subText}>Go to the Explore tab to start a challenge!</Text>
</View>
)
}
if (error) {
return (
<View style={styles.centered}>
<Text style={styles.emptyText}>Something went wrong</Text>
<Text style={styles.subText}>{error}</Text>
<TouchableOpacity onPress={() => setError(null)} style={{ marginTop: 20, padding: 10, backgroundColor: Colors.light.tint, borderRadius: 8 }}>
<Text style={{ color: 'white', fontWeight: 'bold' }}>Retry</Text>
</TouchableOpacity>
</View>
)
}
if (!dailyProgress) {
// If we have a user challenge but no daily progress yet, it might be creating it.
// However, if it takes too long or fails, we should probably show something else or retry.
// For now, let's just log that we are waiting.
console.log('ProgressScreen: waiting for dailyProgress')
return (
<View style={styles.centered}>
<ActivityIndicator size="large" color={Colors.light.tint} />
<Text style={{ marginTop: 10 }}>Loading today{`'`}s progress...</Text>
</View>
)
}
const completedCount = taskCompletions.filter(t => t.completedAt > 0).length
const totalCount = taskCompletions.length
const progressPercentage = totalCount > 0 ? completedCount / totalCount : 0
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.challengeName}>{userChallenge.challenge.name}</Text>
<Text style={styles.dayText}>Day {dailyProgress.dayNumber}</Text>
</View>
<View style={styles.progressContainer}>
<View style={styles.progressBarBackground}>
<View style={[styles.progressBarFill, { width: `${progressPercentage * 100}%` }]} />
</View>
<Text style={styles.progressText}>
{completedCount} of {totalCount} tasks completed
</Text>
</View>
{progressPercentage === 1 && (
<View style={styles.congratsContainer}>
<IconSymbol name="star.fill" size={40} color="#FFD700" />
<Text style={styles.congratsText}>Day Complete! Great Job!</Text>
</View>
)}
<FlatList
data={taskCompletions}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<TaskItem
taskCompletion={item}
onToggle={toggleTaskCompletion}
/>
)}
contentContainerStyle={styles.listContent}
/>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
centered: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
header: {
padding: 20,
backgroundColor: 'white',
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
challengeName: {
fontSize: 24,
fontWeight: 'bold',
color: '#333',
},
dayText: {
fontSize: 18,
color: '#666',
marginTop: 4,
},
progressContainer: {
padding: 20,
backgroundColor: 'white',
marginBottom: 10,
},
progressBarBackground: {
height: 10,
backgroundColor: '#e0e0e0',
borderRadius: 5,
overflow: 'hidden',
},
progressBarFill: {
height: '100%',
backgroundColor: Colors.light.tint,
},
progressText: {
marginTop: 8,
textAlign: 'right',
color: '#666',
},
listContent: {
padding: 16,
},
taskItem: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'white',
padding: 16,
borderRadius: 12,
marginBottom: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05,
shadowRadius: 4,
elevation: 2,
},
checkbox: {
width: 24,
height: 24,
borderRadius: 12,
borderWidth: 2,
borderColor: Colors.light.tint,
marginRight: 16,
justifyContent: 'center',
alignItems: 'center',
},
checkboxChecked: {
backgroundColor: Colors.light.tint,
},
taskContent: {
flex: 1,
},
taskTitle: {
fontSize: 16,
fontWeight: '600',
color: '#333',
},
taskTitleCompleted: {
textDecorationLine: 'line-through',
color: '#999',
},
taskDescription: {
fontSize: 14,
color: '#666',
marginTop: 2,
},
emptyText: {
fontSize: 18,
fontWeight: 'bold',
color: '#333',
marginBottom: 8,
},
subText: {
fontSize: 16,
color: '#666',
textAlign: 'center',
},
congratsContainer: {
backgroundColor: '#E8F5E9',
margin: 20,
marginTop: 0,
padding: 15,
borderRadius: 12,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderWidth: 1,
borderColor: '#4CAF50',
},
congratsText: {
fontSize: 18,
fontWeight: 'bold',
color: '#2E7D32',
marginLeft: 10,
},
})
const enhance = withObservables([], () => {
// We need to observe the active user challenge and today's progress
// This is a bit complex because we need to chain observables
// 1. Get active user challenge
const userChallenge$ = database.get<UserChallenge>('user_challenges')
.query(
Q.where('status', 'active')
)
.observeWithColumns(['status'])
.pipe(
map((challenges: UserChallenge[]) => challenges.length > 0 ? challenges[0] : null)
)
return {
userChallenge: userChallenge$,
dailyProgress: userChallenge$.pipe(
switchMap((uc: UserChallenge | null): Observable<DailyProgress | null> => {
if (!uc) return new Observable<DailyProgress | null>(observer => observer.next(null))
const today = new Date()
today.setHours(0,0,0,0)
const todayTs = today.getTime()
return database.get<DailyProgress>('daily_progress')
.query(
Q.where('user_challenge_id', uc.id),
Q.where('progress_date', todayTs)
)
.observe()
.pipe(
map(progresses => progresses.length > 0 ? progresses[0] : null)
)
})
),
taskCompletions: userChallenge$.pipe(
switchMap((uc: UserChallenge | null): Observable<TaskCompletion[]> => {
if (!uc) return new Observable<TaskCompletion[]>(observer => { observer.next([]); observer.complete() })
const today = new Date()
today.setHours(0,0,0,0)
const todayTs = today.getTime()
return database.get<DailyProgress>('daily_progress')
.query(
Q.where('user_challenge_id', uc.id),
Q.where('progress_date', todayTs)
)
.observe()
.pipe(
switchMap((progresses: DailyProgress[]): Observable<TaskCompletion[]> => {
const todayProgress = progresses.length > 0 ? progresses[0] : null
if (!todayProgress) return new Observable<TaskCompletion[]>(observer => { observer.next([]); observer.complete() })
return todayProgress.taskCompletions.observe()
})
)
})
) as Observable<TaskCompletion[]>
}
})
export default enhance(ProgressScreen)