427 lines
12 KiB
TypeScript
427 lines
12 KiB
TypeScript
import React, { useState, useCallback } from "react";
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
ScrollView,
|
|
RefreshControl,
|
|
TouchableOpacity,
|
|
ActivityIndicator,
|
|
Alert,
|
|
} from "react-native";
|
|
import { LinearGradient } from "expo-linear-gradient";
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
import { useUser } from "@clerk/clerk-expo";
|
|
import { useFocusEffect } from "expo-router";
|
|
import { theme } from "../../styles/theme";
|
|
import { useRecommendations } from "../../contexts/RecommendationsContext";
|
|
import type { Recommendation } from "../../api/recommendations";
|
|
import log from "../../utils/logger";
|
|
|
|
export default function RecommendationsScreen() {
|
|
const { user } = useUser();
|
|
const {
|
|
recommendations: allRecommendations,
|
|
loading,
|
|
refetchRecommendations,
|
|
generateNewRecommendation,
|
|
} = useRecommendations();
|
|
const [generating, setGenerating] = useState(false);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
|
|
// Filter to show only approved recommendations for regular users
|
|
const recommendations = allRecommendations.filter(
|
|
(rec) => rec.status === "approved",
|
|
);
|
|
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
refetchRecommendations();
|
|
}, [refetchRecommendations]),
|
|
);
|
|
|
|
const onRefresh = async () => {
|
|
setRefreshing(true);
|
|
await refetchRecommendations();
|
|
setRefreshing(false);
|
|
};
|
|
|
|
const handleGenerateRecommendation = async () => {
|
|
if (!user?.id) return;
|
|
|
|
Alert.alert(
|
|
"Generate AI Recommendation",
|
|
"Generate a personalized fitness and nutrition plan based on your profile and goals?",
|
|
[
|
|
{ text: "Cancel", style: "cancel" },
|
|
{
|
|
text: "Generate",
|
|
onPress: async () => {
|
|
try {
|
|
setGenerating(true);
|
|
await generateNewRecommendation({
|
|
userId: user.id,
|
|
modelProvider: "openai",
|
|
useExternalModel: true,
|
|
});
|
|
Alert.alert(
|
|
"Success",
|
|
"AI recommendation generated! It will appear here once approved by your trainer.",
|
|
);
|
|
await refetchRecommendations();
|
|
} catch (error) {
|
|
log.error("Failed to generate recommendation", error);
|
|
Alert.alert(
|
|
"Error",
|
|
"Failed to generate recommendation. Please try again.",
|
|
);
|
|
} finally {
|
|
setGenerating(false);
|
|
}
|
|
},
|
|
},
|
|
],
|
|
);
|
|
};
|
|
|
|
if (loading && recommendations.length === 0) {
|
|
return (
|
|
<View style={styles.centered}>
|
|
<ActivityIndicator size="large" color={theme.colors.primary} />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<ScrollView
|
|
contentContainerStyle={styles.scrollContent}
|
|
refreshControl={
|
|
<RefreshControl
|
|
refreshing={refreshing}
|
|
onRefresh={onRefresh}
|
|
tintColor={theme.colors.primary}
|
|
/>
|
|
}
|
|
>
|
|
{/* Header */}
|
|
<LinearGradient
|
|
colors={theme.gradients.primary}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 1 }}
|
|
style={styles.header}
|
|
>
|
|
<View>
|
|
<Text style={styles.headerTitle}>AI Recommendations</Text>
|
|
<Text style={styles.headerSubtitle}>
|
|
Personalized fitness & nutrition plans
|
|
</Text>
|
|
</View>
|
|
<View style={styles.iconContainer}>
|
|
<Ionicons name="sparkles" size={32} color="#fff" />
|
|
</View>
|
|
</LinearGradient>
|
|
|
|
{/* Generate Button */}
|
|
<View style={styles.actionContainer}>
|
|
<TouchableOpacity
|
|
onPress={handleGenerateRecommendation}
|
|
disabled={generating}
|
|
activeOpacity={0.8}
|
|
>
|
|
<LinearGradient
|
|
colors={theme.gradients.purple}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 0 }}
|
|
style={[styles.generateButton, theme.shadows.medium]}
|
|
>
|
|
{generating ? (
|
|
<ActivityIndicator size="small" color="#fff" />
|
|
) : (
|
|
<>
|
|
<Ionicons
|
|
name="bulb"
|
|
size={24}
|
|
color="#fff"
|
|
style={{ marginRight: 12 }}
|
|
/>
|
|
<Text style={styles.generateButtonText}>
|
|
Generate New Plan
|
|
</Text>
|
|
</>
|
|
)}
|
|
</LinearGradient>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Recommendations List */}
|
|
<View style={styles.section}>
|
|
{recommendations.length === 0 ? (
|
|
<View style={styles.emptyState}>
|
|
<LinearGradient
|
|
colors={["rgba(139, 92, 246, 0.1)", "rgba(139, 92, 246, 0.05)"]}
|
|
style={styles.emptyCard}
|
|
>
|
|
<Ionicons
|
|
name="sparkles-outline"
|
|
size={64}
|
|
color={theme.colors.purple}
|
|
/>
|
|
<Text style={styles.emptyTitle}>No Recommendations Yet</Text>
|
|
<Text style={styles.emptyText}>
|
|
Tap "Generate New Plan" to get personalized AI-powered fitness
|
|
and nutrition recommendations based on your profile and goals.
|
|
</Text>
|
|
</LinearGradient>
|
|
</View>
|
|
) : (
|
|
recommendations.map((recommendation) => (
|
|
<RecommendationCard
|
|
key={recommendation.id}
|
|
recommendation={recommendation}
|
|
/>
|
|
))
|
|
)}
|
|
</View>
|
|
</ScrollView>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
interface RecommendationCardProps {
|
|
recommendation: Recommendation;
|
|
}
|
|
|
|
function RecommendationCard({ recommendation }: RecommendationCardProps) {
|
|
const [expanded, setExpanded] = useState(false);
|
|
|
|
return (
|
|
<View style={styles.card}>
|
|
<LinearGradient
|
|
colors={["rgba(255, 255, 255, 1)", "rgba(249, 250, 251, 1)"]}
|
|
style={[styles.cardContent, theme.shadows.medium]}
|
|
>
|
|
{/* Header */}
|
|
<View style={styles.cardHeader}>
|
|
<View style={styles.cardHeaderLeft}>
|
|
<LinearGradient
|
|
colors={theme.gradients.success}
|
|
style={styles.cardIcon}
|
|
>
|
|
<Ionicons name="checkmark-circle" size={20} color="#fff" />
|
|
</LinearGradient>
|
|
<View>
|
|
<Text style={styles.cardTitle}>AI Fitness Plan</Text>
|
|
<Text style={styles.cardDate}>
|
|
{new Date(recommendation.generatedAt).toLocaleDateString()}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
<TouchableOpacity onPress={() => setExpanded(!expanded)}>
|
|
<Ionicons
|
|
name={expanded ? "chevron-up" : "chevron-down"}
|
|
size={24}
|
|
color={theme.colors.gray400}
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Summary */}
|
|
<View style={styles.cardSummary}>
|
|
<Text
|
|
style={styles.summaryText}
|
|
numberOfLines={expanded ? undefined : 3}
|
|
>
|
|
{recommendation.recommendationText}
|
|
</Text>
|
|
</View>
|
|
|
|
{/* Expanded Content */}
|
|
{expanded && (
|
|
<View style={styles.expandedContent}>
|
|
{/* Activity Plan */}
|
|
<View style={styles.planSection}>
|
|
<View style={styles.planHeader}>
|
|
<Ionicons
|
|
name="barbell"
|
|
size={20}
|
|
color={theme.colors.primary}
|
|
/>
|
|
<Text style={styles.planTitle}>Activity Plan</Text>
|
|
</View>
|
|
<Text style={styles.planText}>{recommendation.activityPlan}</Text>
|
|
</View>
|
|
|
|
{/* Diet Plan */}
|
|
<View style={styles.planSection}>
|
|
<View style={styles.planHeader}>
|
|
<Ionicons
|
|
name="restaurant"
|
|
size={20}
|
|
color={theme.colors.success}
|
|
/>
|
|
<Text style={styles.planTitle}>Diet Plan</Text>
|
|
</View>
|
|
<Text style={styles.planText}>{recommendation.dietPlan}</Text>
|
|
</View>
|
|
</View>
|
|
)}
|
|
</LinearGradient>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: theme.colors.background,
|
|
},
|
|
centered: {
|
|
flex: 1,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
backgroundColor: theme.colors.background,
|
|
},
|
|
scrollContent: {
|
|
paddingBottom: 100,
|
|
},
|
|
header: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
padding: 24,
|
|
paddingTop: 60,
|
|
paddingBottom: 24,
|
|
marginBottom: 20,
|
|
borderBottomLeftRadius: theme.borderRadius.xl,
|
|
borderBottomRightRadius: theme.borderRadius.xl,
|
|
},
|
|
headerTitle: {
|
|
fontSize: theme.typography.fontSize["3xl"],
|
|
fontWeight: theme.typography.fontWeight.bold,
|
|
color: theme.colors.white,
|
|
},
|
|
headerSubtitle: {
|
|
fontSize: theme.typography.fontSize.base,
|
|
color: "rgba(255, 255, 255, 0.9)",
|
|
marginTop: 4,
|
|
},
|
|
iconContainer: {
|
|
backgroundColor: "rgba(255, 255, 255, 0.2)",
|
|
width: 64,
|
|
height: 64,
|
|
borderRadius: 32,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
actionContainer: {
|
|
paddingHorizontal: 20,
|
|
marginBottom: 20,
|
|
},
|
|
generateButton: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
paddingVertical: 16,
|
|
paddingHorizontal: 24,
|
|
borderRadius: theme.borderRadius.xl,
|
|
},
|
|
generateButtonText: {
|
|
fontSize: theme.typography.fontSize.lg,
|
|
fontWeight: theme.typography.fontWeight.bold,
|
|
color: theme.colors.white,
|
|
},
|
|
section: {
|
|
paddingHorizontal: 20,
|
|
},
|
|
emptyState: {
|
|
paddingVertical: 40,
|
|
},
|
|
emptyCard: {
|
|
borderRadius: theme.borderRadius["2xl"],
|
|
padding: 32,
|
|
alignItems: "center",
|
|
},
|
|
emptyTitle: {
|
|
fontSize: theme.typography.fontSize.xl,
|
|
fontWeight: theme.typography.fontWeight.bold,
|
|
color: theme.colors.gray700,
|
|
marginTop: 16,
|
|
marginBottom: 8,
|
|
},
|
|
emptyText: {
|
|
fontSize: theme.typography.fontSize.base,
|
|
color: theme.colors.gray500,
|
|
textAlign: "center",
|
|
lineHeight: 24,
|
|
},
|
|
card: {
|
|
marginBottom: 16,
|
|
},
|
|
cardContent: {
|
|
borderRadius: theme.borderRadius["2xl"],
|
|
padding: 20,
|
|
},
|
|
cardHeader: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
marginBottom: 16,
|
|
},
|
|
cardHeaderLeft: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: 12,
|
|
},
|
|
cardIcon: {
|
|
width: 40,
|
|
height: 40,
|
|
borderRadius: 20,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
cardTitle: {
|
|
fontSize: theme.typography.fontSize.lg,
|
|
fontWeight: theme.typography.fontWeight.bold,
|
|
color: theme.colors.gray800,
|
|
},
|
|
cardDate: {
|
|
fontSize: theme.typography.fontSize.sm,
|
|
color: theme.colors.gray500,
|
|
marginTop: 2,
|
|
},
|
|
cardSummary: {
|
|
marginBottom: 12,
|
|
},
|
|
summaryText: {
|
|
fontSize: theme.typography.fontSize.base,
|
|
color: theme.colors.gray700,
|
|
lineHeight: 24,
|
|
},
|
|
expandedContent: {
|
|
marginTop: 12,
|
|
paddingTop: 16,
|
|
borderTopWidth: 1,
|
|
borderTopColor: theme.colors.gray200,
|
|
},
|
|
planSection: {
|
|
marginBottom: 16,
|
|
},
|
|
planHeader: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: 8,
|
|
marginBottom: 8,
|
|
},
|
|
planTitle: {
|
|
fontSize: theme.typography.fontSize.base,
|
|
fontWeight: theme.typography.fontWeight.bold,
|
|
color: theme.colors.gray800,
|
|
},
|
|
planText: {
|
|
fontSize: theme.typography.fontSize.base,
|
|
color: theme.colors.gray600,
|
|
lineHeight: 22,
|
|
},
|
|
});
|