448 lines
13 KiB
TypeScript
448 lines
13 KiB
TypeScript
import React, { useState, useCallback } from "react";
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
ScrollView,
|
|
RefreshControl,
|
|
TouchableOpacity,
|
|
ActivityIndicator,
|
|
Alert,
|
|
} from "react-native";
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
import { useUser } from "@clerk/clerk-expo";
|
|
import { useFocusEffect } from "expo-router";
|
|
import { useTheme } from "../../contexts/ThemeContext";
|
|
import { MinimalCard } from "../../components/MinimalCard";
|
|
import { SectionHeader } from "../../components/SectionHeader";
|
|
import { MinimalButton } from "../../components/MinimalButton";
|
|
import { Badge } from "../../components/Badge";
|
|
import { IconContainer } from "../../components/IconContainer";
|
|
import { useRecommendations } from "../../contexts/RecommendationsContext";
|
|
import { useNotifications } from "../../contexts/NotificationsContext";
|
|
import { NotificationsModal } from "../../components/NotificationsModal";
|
|
import type { Recommendation } from "../../api/recommendations";
|
|
import log from "../../utils/logger";
|
|
|
|
export default function RecommendationsScreen() {
|
|
const { user } = useUser();
|
|
const { colors, typography } = useTheme();
|
|
const {
|
|
recommendations: allRecommendations,
|
|
loading,
|
|
refetchRecommendations,
|
|
generateNewRecommendation,
|
|
} = useRecommendations();
|
|
const { unreadCount, refetchNotifications } = useNotifications();
|
|
const [generating, setGenerating] = useState(false);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const [notificationsVisible, setNotificationsVisible] = useState(false);
|
|
|
|
const handleOpenNotifications = () => {
|
|
log.debug("Opening notifications modal", { unreadCount });
|
|
setNotificationsVisible(true);
|
|
};
|
|
|
|
const handleCloseNotifications = () => {
|
|
log.debug("Closing notifications modal");
|
|
setNotificationsVisible(false);
|
|
};
|
|
|
|
// Filter to show only approved recommendations
|
|
const recommendations = allRecommendations.filter(
|
|
(rec) => rec.status === "approved",
|
|
);
|
|
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
refetchRecommendations();
|
|
refetchNotifications();
|
|
}, [refetchRecommendations, refetchNotifications]),
|
|
);
|
|
|
|
const onRefresh = async () => {
|
|
setRefreshing(true);
|
|
await Promise.all([refetchRecommendations(), refetchNotifications()]);
|
|
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: "deepseek",
|
|
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, { backgroundColor: colors.background }]}>
|
|
<ActivityIndicator size="large" color={colors.primary} />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
|
<NotificationsModal
|
|
visible={notificationsVisible}
|
|
onClose={handleCloseNotifications}
|
|
/>
|
|
<ScrollView
|
|
contentContainerStyle={styles.scrollContent}
|
|
refreshControl={
|
|
<RefreshControl
|
|
refreshing={refreshing}
|
|
onRefresh={onRefresh}
|
|
tintColor={colors.primary}
|
|
/>
|
|
}
|
|
>
|
|
{/* Header */}
|
|
<View style={styles.header}>
|
|
<View style={{ flex: 1 }}>
|
|
<Text
|
|
style={[
|
|
typography.h1,
|
|
{ color: colors.textPrimary, fontSize: 32 },
|
|
]}
|
|
>
|
|
Recommendations
|
|
</Text>
|
|
<Text
|
|
style={[
|
|
typography.body,
|
|
{ color: colors.textSecondary, marginTop: 8 },
|
|
]}
|
|
>
|
|
{recommendations.length === 0
|
|
? "Let's create your perfect plan!"
|
|
: `${recommendations.length} plan${recommendations.length !== 1 ? "s" : ""} ready for you!`}
|
|
</Text>
|
|
</View>
|
|
<TouchableOpacity
|
|
style={styles.notificationButton}
|
|
onPress={handleOpenNotifications}
|
|
activeOpacity={0.7}
|
|
>
|
|
<IconContainer
|
|
variant="colored"
|
|
backgroundColor={colors.accent}
|
|
size="lg"
|
|
>
|
|
<Ionicons name="sparkles" size={24} color={colors.white} />
|
|
{unreadCount > 0 && (
|
|
<View
|
|
style={[
|
|
styles.notificationBadge,
|
|
{ backgroundColor: colors.danger },
|
|
]}
|
|
>
|
|
<Text style={styles.notificationBadgeText}>
|
|
{unreadCount}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
</IconContainer>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Generate Button */}
|
|
<View style={styles.section}>
|
|
<MinimalButton
|
|
title="Generate New Plan"
|
|
onPress={handleGenerateRecommendation}
|
|
variant="primary"
|
|
size="lg"
|
|
fullWidth
|
|
loading={generating}
|
|
disabled={generating}
|
|
textStyle={{ fontSize: 16 }}
|
|
/>
|
|
</View>
|
|
|
|
{/* Recommendations List */}
|
|
<View style={styles.section}>
|
|
<SectionHeader
|
|
title="💡 Your Plans"
|
|
subtitle={
|
|
recommendations.length > 0
|
|
? `${recommendations.length} active plan${recommendations.length !== 1 ? "s" : ""}`
|
|
: undefined
|
|
}
|
|
/>
|
|
|
|
{recommendations.length === 0 ? (
|
|
<MinimalCard variant="default" style={{ borderRadius: 20 }}>
|
|
<View style={styles.emptyState}>
|
|
<Text style={{ fontSize: 64 }}>🤖</Text>
|
|
<Text
|
|
style={[
|
|
typography.bodyEmphasis,
|
|
{ color: colors.textPrimary, marginTop: 16 },
|
|
]}
|
|
>
|
|
No Recommendations Yet
|
|
</Text>
|
|
<Text
|
|
style={[
|
|
typography.body,
|
|
{
|
|
color: colors.textSecondary,
|
|
marginTop: 8,
|
|
textAlign: "center",
|
|
},
|
|
]}
|
|
>
|
|
Tap "Generate New Plan" to get personalized AI-powered fitness
|
|
and nutrition recommendations! 🎯
|
|
</Text>
|
|
</View>
|
|
</MinimalCard>
|
|
) : (
|
|
<View style={styles.recommendationsList}>
|
|
{recommendations.map((recommendation) => (
|
|
<RecommendationCard
|
|
key={recommendation.id}
|
|
recommendation={recommendation}
|
|
/>
|
|
))}
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
{/* Bottom Spacer */}
|
|
<View style={{ height: 100 }} />
|
|
</ScrollView>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
interface RecommendationCardProps {
|
|
recommendation: Recommendation;
|
|
}
|
|
|
|
function RecommendationCard({ recommendation }: RecommendationCardProps) {
|
|
const { colors, typography } = useTheme();
|
|
const [expanded, setExpanded] = useState(false);
|
|
|
|
return (
|
|
<MinimalCard variant="bordered" style={{ borderRadius: 20 }}>
|
|
{/* Header */}
|
|
<TouchableOpacity
|
|
onPress={() => setExpanded(!expanded)}
|
|
activeOpacity={0.7}
|
|
style={styles.cardHeader}
|
|
>
|
|
<View style={styles.cardHeaderLeft}>
|
|
<IconContainer
|
|
variant="colored"
|
|
backgroundColor={`${colors.success}20`}
|
|
>
|
|
<Ionicons
|
|
name="checkmark-circle"
|
|
size={20}
|
|
color={colors.success}
|
|
/>
|
|
</IconContainer>
|
|
<View style={{ marginLeft: 12 }}>
|
|
<Text style={[typography.h3, { color: colors.textPrimary }]}>
|
|
✨ AI Fitness Plan
|
|
</Text>
|
|
<Text style={[typography.caption, { color: colors.textTertiary }]}>
|
|
{new Date(recommendation.generatedAt).toLocaleDateString()}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
<Ionicons
|
|
name={expanded ? "chevron-up" : "chevron-down"}
|
|
size={24}
|
|
color={colors.textTertiary}
|
|
/>
|
|
</TouchableOpacity>
|
|
|
|
{/* Summary */}
|
|
<View style={styles.cardSummary}>
|
|
<Text
|
|
style={[typography.body, { color: colors.textSecondary }]}
|
|
numberOfLines={expanded ? undefined : 3}
|
|
>
|
|
{recommendation.recommendationText}
|
|
</Text>
|
|
</View>
|
|
|
|
{/* Expanded Content */}
|
|
{expanded && (
|
|
<View style={styles.expandedContent}>
|
|
{/* Activity Plan */}
|
|
<View
|
|
style={[
|
|
styles.planSection,
|
|
{ backgroundColor: colors.surfaceElevated },
|
|
]}
|
|
>
|
|
<View style={styles.planHeader}>
|
|
<Ionicons name="barbell" size={20} color={colors.primary} />
|
|
<Text
|
|
style={[
|
|
typography.h3,
|
|
{ color: colors.textPrimary, marginLeft: 8 },
|
|
]}
|
|
>
|
|
💪 Activity Plan
|
|
</Text>
|
|
</View>
|
|
<Text
|
|
style={[
|
|
typography.body,
|
|
{ color: colors.textSecondary, marginTop: 8 },
|
|
]}
|
|
>
|
|
{recommendation.activityPlan}
|
|
</Text>
|
|
</View>
|
|
|
|
{/* Diet Plan */}
|
|
<View
|
|
style={[
|
|
styles.planSection,
|
|
{ backgroundColor: colors.surfaceElevated },
|
|
]}
|
|
>
|
|
<View style={styles.planHeader}>
|
|
<Ionicons name="restaurant" size={20} color={colors.success} />
|
|
<Text
|
|
style={[
|
|
typography.h3,
|
|
{ color: colors.textPrimary, marginLeft: 8 },
|
|
]}
|
|
>
|
|
🍽️ Diet Plan
|
|
</Text>
|
|
</View>
|
|
<Text
|
|
style={[
|
|
typography.body,
|
|
{ color: colors.textSecondary, marginTop: 8 },
|
|
]}
|
|
>
|
|
{recommendation.dietPlan}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
)}
|
|
</MinimalCard>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
},
|
|
centered: {
|
|
flex: 1,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
scrollContent: {
|
|
paddingBottom: 20,
|
|
},
|
|
header: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
alignItems: "flex-start",
|
|
paddingHorizontal: 24,
|
|
paddingTop: 60,
|
|
paddingBottom: 24,
|
|
},
|
|
notificationButton: {
|
|
position: "relative",
|
|
},
|
|
notificationBadge: {
|
|
position: "absolute",
|
|
top: -4,
|
|
right: -4,
|
|
minWidth: 18,
|
|
height: 18,
|
|
borderRadius: 9,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
paddingHorizontal: 4,
|
|
},
|
|
notificationBadgeText: {
|
|
color: "#fff",
|
|
fontSize: 11,
|
|
fontWeight: "700",
|
|
},
|
|
section: {
|
|
paddingHorizontal: 24,
|
|
marginBottom: 24,
|
|
},
|
|
emptyState: {
|
|
alignItems: "center",
|
|
paddingVertical: 40,
|
|
paddingHorizontal: 20,
|
|
},
|
|
recommendationsList: {
|
|
gap: 12,
|
|
},
|
|
cardHeader: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
},
|
|
cardHeaderLeft: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
flex: 1,
|
|
},
|
|
cardSummary: {
|
|
marginTop: 12,
|
|
paddingTop: 12,
|
|
borderTopWidth: 1,
|
|
borderTopColor: "rgba(0, 0, 0, 0.05)",
|
|
},
|
|
expandedContent: {
|
|
marginTop: 16,
|
|
gap: 12,
|
|
},
|
|
planSection: {
|
|
padding: 16,
|
|
borderRadius: 16,
|
|
},
|
|
planHeader: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
},
|
|
});
|