import React, { useEffect, useState } from 'react'; import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Alert, ActivityIndicator } from 'react-native'; import { useLocalSearchParams, useRouter } from 'expo-router'; import { withObservables } from '@nozbe/watermelondb/react'; import { database } from '../../src/db'; import Challenge from '../../src/db/models/Challenge'; import ChallengeRequirement from '../../src/db/models/ChallengeRequirement'; import { syncChallengeDetails } from '../../src/db/sync'; import UserChallenge from '../../src/db/models/UserChallenge'; import User from '../../src/db/models/User'; const ChallengeDetailScreen = ({ challenge, requirements }: { challenge: Challenge; requirements: ChallengeRequirement[] }) => { const router = useRouter(); const [loading, setLoading] = useState(false); const [starting, setStarting] = useState(false); useEffect(() => { if (challenge) { setLoading(true); syncChallengeDetails(challenge.id).finally(() => setLoading(false)); } }, [challenge]); const handleStartChallenge = async () => { if (!challenge) return; setStarting(true); try { await database.write(async () => { const userChallengesCollection = database.get('user_challenges'); // Check if already active // Note: In a real app, we'd check for existing active challenges for this user // For now, we'll just create a new one. // We need a user ID. Since we don't have auth fully set up in this context, // we'll use a placeholder or fetch the first user if available. const usersCollection = database.get('users'); const users = await usersCollection.query().fetch(); let userId = 'temp-user-id'; if (users.length > 0) { userId = users[0].id; } else { // Create a temp user if none exists (for testing) const newUser = await usersCollection.create(user => { user.firebaseUid = 'temp-uid'; user.email = 'test@example.com'; }); userId = newUser.id; } await userChallengesCollection.create(uc => { uc.user.id = userId; uc.challenge.set(challenge); uc.startDate = new Date().getTime(); uc.status = 'active'; uc.currentStreak = 0; uc.longestStreak = 0; uc.attemptNumber = 1; uc.createdAt = new Date().getTime(); }); }); Alert.alert('Success', 'Challenge started!', [ { text: 'OK', onPress: () => router.replace('/') } ]); } catch (error) { console.error('Error starting challenge:', error); Alert.alert('Error', 'Failed to start challenge.'); } finally { setStarting(false); } }; if (!challenge) { return ( Loading challenge... ); } return ( {challenge.name} {challenge.difficulty} {challenge.durationDays} Days {challenge.description} Daily Requirements {loading && requirements.length === 0 ? ( ) : ( requirements.map((req, index) => ( {index + 1} {req.title} {req.description ? {req.description} : null} )) )} {starting ? ( ) : ( Start Challenge )} ); }; const enhance = withObservables(['id'], ({ id }: { id: string }) => ({ challenge: database.get('challenges').findAndObserve(id), requirements: database.get('challenge_requirements') .query( // We can't easily query by relation in withObservables without Q.on // But since we are syncing details which updates requirements, // we can just query by challenge_id if we had the challenge object available immediately. // However, 'challenge' prop is async. // A common pattern is to pass the challenge itself or query requirements based on the ID prop. ).observeWithColumns(['title', 'description', 'sort_order']) // Wait, the above query is empty. We need to filter by challenge_id. // But we only have `id` (challenge id) from props. })); // Correct way to query related records with WatermelonDB observables const enhanceWithRelated = withObservables(['id'], ({ id }: { id: string }) => { const challengeObservable = database.get('challenges').findAndObserve(id); // We need to return an object where keys are prop names and values are observables return { challenge: challengeObservable, // To get requirements, we can't use `challenge.requirements` directly here because `challenge` is an observable, not the record yet. // But we can query the requirements table directly using the ID. requirements: database.get('challenge_requirements') .query( // @ts-ignore // We need to import Q from watermelondb // But for now let's assume the query is correct require('@nozbe/watermelondb').Q.where('challenge_id', id), require('@nozbe/watermelondb').Q.sortBy('sort_order', require('@nozbe/watermelondb').Q.asc) ) }; }); export default function ChallengeDetailWrapper() { const { id } = useLocalSearchParams(); const ChallengeDetail = enhanceWithRelated(ChallengeDetailScreen); // Ensure ID is a string const challengeId = Array.isArray(id) ? id[0] : id; if (!challengeId) return null; return ; } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', padding: 20, }, loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', }, header: { marginBottom: 20, }, title: { fontSize: 28, fontWeight: 'bold', color: '#333', marginBottom: 10, }, metaContainer: { flexDirection: 'row', alignItems: 'center', }, badge: { paddingHorizontal: 10, paddingVertical: 5, borderRadius: 15, marginRight: 10, }, badgeText: { fontSize: 12, fontWeight: 'bold', color: '#555', textTransform: 'uppercase', }, duration: { fontSize: 14, color: '#666', }, description: { fontSize: 16, color: '#444', lineHeight: 24, marginBottom: 30, }, section: { marginBottom: 30, }, sectionTitle: { fontSize: 20, fontWeight: '600', marginBottom: 15, color: '#333', }, requirementItem: { flexDirection: 'row', marginBottom: 15, backgroundColor: '#f9f9f9', padding: 15, borderRadius: 10, }, reqIndex: { fontSize: 16, fontWeight: 'bold', color: '#007AFF', marginRight: 15, width: 24, }, reqContent: { flex: 1, }, reqTitle: { fontSize: 16, fontWeight: '500', color: '#333', marginBottom: 4, }, reqDesc: { fontSize: 14, color: '#666', }, startButton: { backgroundColor: '#007AFF', paddingVertical: 16, borderRadius: 12, alignItems: 'center', marginBottom: 40, }, disabledButton: { opacity: 0.7, }, startButtonText: { color: 'white', fontSize: 18, fontWeight: 'bold', }, });