345 lines
10 KiB
TypeScript
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) |