Files
fitaiProto/apps/mobile/src/app/(tabs)/recommendations.tsx
T
dimitar 5d6166df1b redesign take 2 complete
fix artefacts from previous dessign
2026-03-12 17:56:46 +01:00

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",
},
});