272 lines
8.4 KiB
TypeScript
272 lines
8.4 KiB
TypeScript
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<UserChallenge>('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<User>('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 (
|
|
<View style={styles.loadingContainer}>
|
|
<Text>Loading challenge...</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ScrollView style={styles.container}>
|
|
<View style={styles.header}>
|
|
<Text style={styles.title}>{challenge.name}</Text>
|
|
<View style={styles.metaContainer}>
|
|
<View style={[styles.badge, { backgroundColor: '#E0E0E0' }]}>
|
|
<Text style={styles.badgeText}>{challenge.difficulty}</Text>
|
|
</View>
|
|
<Text style={styles.duration}>{challenge.durationDays} Days</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<Text style={styles.description}>{challenge.description}</Text>
|
|
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>Daily Requirements</Text>
|
|
{loading && requirements.length === 0 ? (
|
|
<ActivityIndicator size="small" color="#0000ff" />
|
|
) : (
|
|
requirements.map((req, index) => (
|
|
<View key={req.id} style={styles.requirementItem}>
|
|
<Text style={styles.reqIndex}>{index + 1}</Text>
|
|
<View style={styles.reqContent}>
|
|
<Text style={styles.reqTitle}>{req.title}</Text>
|
|
{req.description ? <Text style={styles.reqDesc}>{req.description}</Text> : null}
|
|
</View>
|
|
</View>
|
|
))
|
|
)}
|
|
</View>
|
|
|
|
<TouchableOpacity
|
|
style={[styles.startButton, starting && styles.disabledButton]}
|
|
onPress={handleStartChallenge}
|
|
disabled={starting}
|
|
>
|
|
{starting ? (
|
|
<ActivityIndicator color="white" />
|
|
) : (
|
|
<Text style={styles.startButtonText}>Start Challenge</Text>
|
|
)}
|
|
</TouchableOpacity>
|
|
</ScrollView>
|
|
);
|
|
};
|
|
|
|
const enhance = withObservables(['id'], ({ id }: { id: string }) => ({
|
|
challenge: database.get<Challenge>('challenges').findAndObserve(id),
|
|
requirements: database.get<ChallengeRequirement>('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<Challenge>('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<ChallengeRequirement>('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 <ChallengeDetail id={challengeId} />;
|
|
}
|
|
|
|
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',
|
|
},
|
|
}); |