3 and 4 completed
This commit is contained in:
1 parent
3573709d99
commit
97436c6823
21 files changed
+2168
-558
No files matched your search
@@ -12,6 +12,7 @@ import { useAuth } from "@clerk/clerk-expo";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { attendanceApi, Attendance } from "../../api/attendance";
|
||||
import { AttendanceCalendar } from "../../components/AttendanceCalendar";
|
||||
import { theme } from "../../styles/theme";
|
||||
import { Animated } from "react-native";
|
||||
import { getErrorMessage } from "../../utils/error-helpers";
|
||||
@@ -186,6 +187,9 @@ export default function AttendanceScreen() {
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Attendance Calendar */}
|
||||
{history.length > 0 && <AttendanceCalendar attendanceRecords={history} />}
|
||||
|
||||
<Text style={styles.sectionTitle}>Recent History</Text>
|
||||
{history.map((item) => (
|
||||
<LinearGradient
|
||||
|
||||
@@ -14,12 +14,16 @@ import { LinearGradient } from "expo-linear-gradient";
|
||||
import { theme } from "../../styles/theme";
|
||||
import { GoalProgressCard } from "../../components/GoalProgressCard";
|
||||
import { GoalCreationModal } from "../../components/GoalCreationModal";
|
||||
import { WeeklyProgressChart } from "../../components/WeeklyProgressChart";
|
||||
import { GoalTypeBreakdownChart } from "../../components/GoalTypeBreakdownChart";
|
||||
import { useUser, useAuth } from "@clerk/clerk-expo";
|
||||
import {
|
||||
fitnessGoalsService,
|
||||
type FitnessGoal,
|
||||
type CreateGoalData,
|
||||
} from "../../services/fitnessGoals";
|
||||
import { getUserStatistics } from "../../api/statistics";
|
||||
import type { UserStatisticsResponse } from "../../api/types";
|
||||
import { useFocusEffect } from "expo-router";
|
||||
import * as SecureStore from "expo-secure-store";
|
||||
import log from "../../utils/logger";
|
||||
@@ -28,8 +32,12 @@ export default function GoalsScreen() {
|
||||
const { user } = useUser();
|
||||
const { getToken } = useAuth();
|
||||
const [goals, setGoals] = useState<FitnessGoal[]>([]);
|
||||
const [statistics, setStatistics] = useState<UserStatisticsResponse | null>(
|
||||
null,
|
||||
);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [showAnalytics, setShowAnalytics] = useState(false);
|
||||
const fabScale = useRef(new Animated.Value(1)).current;
|
||||
|
||||
const loadGoals = useCallback(async () => {
|
||||
@@ -60,6 +68,14 @@ export default function GoalsScreen() {
|
||||
|
||||
const loadedGoals = await fitnessGoalsService.getGoals(user.id, token);
|
||||
setGoals(loadedGoals);
|
||||
|
||||
// Load statistics
|
||||
try {
|
||||
const stats = await getUserStatistics(user.id);
|
||||
setStatistics(stats);
|
||||
} catch (error) {
|
||||
log.error("Failed to load statistics", error);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error("Failed to load goals", error);
|
||||
}
|
||||
@@ -199,6 +215,47 @@ export default function GoalsScreen() {
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Analytics Section */}
|
||||
{statistics && (
|
||||
<View style={styles.analyticsSection}>
|
||||
<TouchableOpacity
|
||||
style={styles.analyticsHeader}
|
||||
onPress={() => setShowAnalytics(!showAnalytics)}
|
||||
>
|
||||
<View style={styles.analyticsHeaderLeft}>
|
||||
<Ionicons
|
||||
name="bar-chart-outline"
|
||||
size={20}
|
||||
color={theme.colors.primary}
|
||||
/>
|
||||
<Text style={styles.analyticsTitle}>Progress Analytics</Text>
|
||||
</View>
|
||||
<Ionicons
|
||||
name={showAnalytics ? "chevron-up" : "chevron-down"}
|
||||
size={20}
|
||||
color={theme.colors.gray400}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
{showAnalytics && (
|
||||
<View style={styles.analyticsContent}>
|
||||
{statistics.weeklyTrend.length > 0 && (
|
||||
<WeeklyProgressChart
|
||||
weeklyData={statistics.weeklyTrend}
|
||||
title="8-Week Trend"
|
||||
/>
|
||||
)}
|
||||
{statistics.goals.goalsByType.length > 0 && (
|
||||
<GoalTypeBreakdownChart
|
||||
data={statistics.goals.goalsByType}
|
||||
title="Goals by Type"
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Active Goals */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>
|
||||
@@ -348,6 +405,33 @@ const styles = StyleSheet.create({
|
||||
color: "#6b7280",
|
||||
fontWeight: "500",
|
||||
},
|
||||
analyticsSection: {
|
||||
padding: 16,
|
||||
paddingTop: 0,
|
||||
},
|
||||
analyticsHeader: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
backgroundColor: theme.colors.white,
|
||||
padding: 16,
|
||||
borderRadius: theme.borderRadius.xl,
|
||||
marginBottom: 12,
|
||||
...theme.shadows.subtle,
|
||||
},
|
||||
analyticsHeaderLeft: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
},
|
||||
analyticsTitle: {
|
||||
fontSize: theme.typography.fontSize.base,
|
||||
fontWeight: theme.typography.fontWeight.semibold,
|
||||
color: theme.colors.gray700,
|
||||
},
|
||||
analyticsContent: {
|
||||
paddingTop: 4,
|
||||
},
|
||||
section: {
|
||||
padding: 20,
|
||||
paddingTop: 10,
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import { View, Text, StyleSheet, ScrollView, RefreshControl, Image } from "react-native";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
RefreshControl,
|
||||
Image,
|
||||
} from "react-native";
|
||||
import { useUser } from "@clerk/clerk-expo";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { theme } from "../../styles/theme";
|
||||
import { ActivityWidget } from "../../components/ActivityWidget";
|
||||
import { QuickActionGrid } from "../../components/QuickActionGrid";
|
||||
import { TrackMealModal } from "../../components/TrackMealModal";
|
||||
import { AddWaterModal } from "../../components/AddWaterModal";
|
||||
import { HydrationWidget } from "../../components/HydrationWidget";
|
||||
import { WeeklyProgressWidget } from "../../components/WeeklyProgressWidget";
|
||||
import { ScanFoodModal } from "../../components/ScanFoodModal";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
|
||||
@@ -35,13 +43,17 @@ export default function HomeScreen() {
|
||||
return "Good Evening";
|
||||
};
|
||||
|
||||
const handleSaveMeal = (meal: { type: string; name: string; calories: number }) => {
|
||||
setCalories(prev => prev + meal.calories);
|
||||
const handleSaveMeal = (meal: {
|
||||
type: string;
|
||||
name: string;
|
||||
calories: number;
|
||||
}) => {
|
||||
setCalories((prev) => prev + meal.calories);
|
||||
setTrackMealModalVisible(false);
|
||||
};
|
||||
|
||||
const handleAddWater = (amount: number) => {
|
||||
setWaterIntake(prev => prev + amount);
|
||||
setWaterIntake((prev) => prev + amount);
|
||||
setAddWaterModalVisible(false);
|
||||
};
|
||||
|
||||
@@ -54,7 +66,7 @@ export default function HomeScreen() {
|
||||
};
|
||||
|
||||
const handleAddScannedFood = (scannedCalories: number) => {
|
||||
setCalories(prev => prev + scannedCalories);
|
||||
setCalories((prev) => prev + scannedCalories);
|
||||
setScanFoodModalVisible(false);
|
||||
};
|
||||
|
||||
@@ -62,13 +74,13 @@ export default function HomeScreen() {
|
||||
setCalories(0);
|
||||
setWaterIntake(0);
|
||||
const today = new Date().toDateString();
|
||||
await AsyncStorage.setItem('lastResetDate', today);
|
||||
await AsyncStorage.setItem("lastResetDate", today);
|
||||
};
|
||||
|
||||
// Check for midnight reset
|
||||
useEffect(() => {
|
||||
const checkAndResetIfNeeded = async () => {
|
||||
const lastResetDate = await AsyncStorage.getItem('lastResetDate');
|
||||
const lastResetDate = await AsyncStorage.getItem("lastResetDate");
|
||||
const today = new Date().toDateString();
|
||||
|
||||
if (lastResetDate !== today) {
|
||||
@@ -89,9 +101,12 @@ export default function HomeScreen() {
|
||||
await resetAllCounters();
|
||||
|
||||
// Set up daily interval after first midnight
|
||||
const dailyInterval = setInterval(async () => {
|
||||
await resetAllCounters();
|
||||
}, 24 * 60 * 60 * 1000); // 24 hours
|
||||
const dailyInterval = setInterval(
|
||||
async () => {
|
||||
await resetAllCounters();
|
||||
},
|
||||
24 * 60 * 60 * 1000,
|
||||
); // 24 hours
|
||||
|
||||
return () => clearInterval(dailyInterval);
|
||||
}, timeUntilMidnight);
|
||||
@@ -104,7 +119,11 @@ export default function HomeScreen() {
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={theme.colors.primary} />
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
tintColor={theme.colors.primary}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{/* Header Section */}
|
||||
@@ -125,24 +144,7 @@ export default function HomeScreen() {
|
||||
</View>
|
||||
|
||||
{/* Activity Widget */}
|
||||
<ActivityWidget
|
||||
steps={8432}
|
||||
calories={calories}
|
||||
duration={45}
|
||||
/>
|
||||
|
||||
{/* Hydration Widget */}
|
||||
<HydrationWidget
|
||||
current={waterIntake}
|
||||
goal={2500}
|
||||
/>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<QuickActionGrid
|
||||
onTrackMealPress={() => setTrackMealModalVisible(true)}
|
||||
onAddWaterPress={() => setAddWaterModalVisible(true)}
|
||||
onScanFoodPress={() => setScanFoodModalVisible(true)}
|
||||
/>
|
||||
<ActivityWidget calories={calories} />
|
||||
|
||||
<TrackMealModal
|
||||
visible={trackMealModalVisible}
|
||||
@@ -173,7 +175,7 @@ export default function HomeScreen() {
|
||||
|
||||
<View style={styles.activityCard}>
|
||||
<LinearGradient
|
||||
colors={['rgba(255, 255, 255, 0.8)', 'rgba(255, 255, 255, 0.5)']}
|
||||
colors={["rgba(255, 255, 255, 0.8)", "rgba(255, 255, 255, 0.5)"]}
|
||||
style={[styles.recentItem, theme.shadows.subtle]}
|
||||
>
|
||||
<View style={styles.recentIconContainer}>
|
||||
@@ -192,7 +194,7 @@ export default function HomeScreen() {
|
||||
</LinearGradient>
|
||||
|
||||
<LinearGradient
|
||||
colors={['rgba(255, 255, 255, 0.8)', 'rgba(255, 255, 255, 0.5)']}
|
||||
colors={["rgba(255, 255, 255, 0.8)", "rgba(255, 255, 255, 0.5)"]}
|
||||
style={[styles.recentItem, theme.shadows.subtle]}
|
||||
>
|
||||
<View style={styles.recentIconContainer}>
|
||||
|
||||
@@ -1,88 +1,101 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import React, { useState, useCallback } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
FlatList,
|
||||
ActivityIndicator,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
RefreshControl,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
} from "react-native";
|
||||
import { useAuth } from "@clerk/clerk-expo";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { API_BASE_URL, API_ENDPOINTS } from "../../config/api";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useUser } from "@clerk/clerk-expo";
|
||||
import { useFocusEffect } from "expo-router";
|
||||
import { theme } from "../../styles/theme";
|
||||
import {
|
||||
getRecommendations,
|
||||
generateRecommendation,
|
||||
type Recommendation,
|
||||
} from "../../api/recommendations";
|
||||
import log from "../../utils/logger";
|
||||
|
||||
interface Recommendation {
|
||||
id: string;
|
||||
userId: string;
|
||||
recommendationText: string;
|
||||
activityPlan?: string;
|
||||
dietPlan?: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export default function RecommendationsScreen() {
|
||||
const { getToken, userId } = useAuth();
|
||||
const { user } = useUser();
|
||||
const [recommendations, setRecommendations] = useState<Recommendation[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const fetchRecommendations = async () => {
|
||||
const loadRecommendations = useCallback(async () => {
|
||||
if (!user?.id) return;
|
||||
|
||||
try {
|
||||
if (!userId) {
|
||||
log.warn("No userId available");
|
||||
return;
|
||||
}
|
||||
|
||||
const token = await getToken();
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
if (token) {
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const url = `${API_BASE_URL}${API_ENDPOINTS.RECOMMENDATIONS}?userId=${userId}`;
|
||||
log.apiRequest("GET", url);
|
||||
|
||||
const res = await fetch(url, { headers });
|
||||
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text();
|
||||
log.error("API error fetching recommendations", {
|
||||
status: res.status,
|
||||
errorText,
|
||||
});
|
||||
throw new Error(`Network response was not ok: ${res.status}`);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
log.debug("Recommendations fetched", {
|
||||
count: data.recommendations?.length || data.length || 0,
|
||||
});
|
||||
setRecommendations(data.recommendations || data || []);
|
||||
} catch (e) {
|
||||
log.error("Failed to load recommendations", e);
|
||||
setLoading(true);
|
||||
const data = await getRecommendations(user.id);
|
||||
// Filter to show only approved recommendations for regular users
|
||||
const approved = data.filter((rec) => rec.status === "approved");
|
||||
setRecommendations(approved);
|
||||
} catch (error) {
|
||||
log.error("Failed to load recommendations", error);
|
||||
Alert.alert("Error", "Failed to load recommendations");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
}, [user?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchRecommendations();
|
||||
}, []);
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
loadRecommendations();
|
||||
}, [loadRecommendations]),
|
||||
);
|
||||
|
||||
const onRefresh = () => {
|
||||
const onRefresh = async () => {
|
||||
setRefreshing(true);
|
||||
fetchRecommendations();
|
||||
await loadRecommendations();
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
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 generateRecommendation({
|
||||
userId: user.id,
|
||||
modelProvider: "openai",
|
||||
useExternalModel: true,
|
||||
});
|
||||
Alert.alert(
|
||||
"Success",
|
||||
"AI recommendation generated! It will appear here once approved by your trainer.",
|
||||
);
|
||||
await loadRecommendations();
|
||||
} 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} />
|
||||
@@ -92,101 +105,179 @@ export default function RecommendationsScreen() {
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<LinearGradient
|
||||
colors={theme.gradients.primary}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={styles.header}
|
||||
>
|
||||
<Text style={styles.headerTitle}>AI Recommendations</Text>
|
||||
</LinearGradient>
|
||||
|
||||
{/* AI Context Info Banner with Glassmorphism */}
|
||||
<LinearGradient
|
||||
colors={
|
||||
["rgba(59, 130, 246, 0.15)", "rgba(139, 92, 246, 0.1)"] as const
|
||||
}
|
||||
style={styles.infoBanner}
|
||||
>
|
||||
<View style={styles.infoBannerIconContainer}>
|
||||
<LinearGradient
|
||||
colors={theme.gradients.primary}
|
||||
style={styles.infoBannerIcon}
|
||||
>
|
||||
<Ionicons name="sparkles" size={16} color="#fff" />
|
||||
</LinearGradient>
|
||||
</View>
|
||||
<Text style={styles.infoBannerText}>
|
||||
Personalized based on your active fitness goals and progress
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
|
||||
<FlatList
|
||||
data={recommendations}
|
||||
keyExtractor={(item) => item.id}
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
tintColor={theme.colors.primary}
|
||||
/>
|
||||
}
|
||||
contentContainerStyle={styles.listContent}
|
||||
ListEmptyComponent={
|
||||
<View style={styles.emptyContainer}>
|
||||
<View style={styles.emptyIconContainer}>
|
||||
<LinearGradient
|
||||
colors={
|
||||
[
|
||||
"rgba(209, 213, 219, 0.3)",
|
||||
"rgba(209, 213, 219, 0.1)",
|
||||
] as const
|
||||
}
|
||||
style={styles.emptyIconGradient}
|
||||
>
|
||||
<Ionicons name="sparkles-outline" size={48} color="#9ca3af" />
|
||||
</LinearGradient>
|
||||
</View>
|
||||
<Text style={styles.empty}>No recommendations available yet.</Text>
|
||||
<Text style={styles.emptySub}>Pull down to refresh</Text>
|
||||
>
|
||||
{/* 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>
|
||||
}
|
||||
renderItem={({ item }) => (
|
||||
<LinearGradient
|
||||
colors={
|
||||
["rgba(255, 255, 255, 1)", "rgba(249, 250, 251, 1)"] as const
|
||||
}
|
||||
style={[styles.card, theme.shadows.medium]}
|
||||
<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}
|
||||
>
|
||||
<View style={styles.cardHeader}>
|
||||
<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={theme.gradients.success}
|
||||
style={styles.statusBadge}
|
||||
colors={["rgba(139, 92, 246, 0.1)", "rgba(139, 92, 246, 0.05)"]}
|
||||
style={styles.emptyCard}
|
||||
>
|
||||
<Text style={styles.statusText}>
|
||||
{item.status.toUpperCase()}
|
||||
<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>
|
||||
<Text style={styles.date}>
|
||||
{new Date(item.createdAt).toLocaleDateString()}
|
||||
</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>
|
||||
|
||||
<Text style={styles.sectionTitle}>Daily Advice</Text>
|
||||
<Text style={styles.content}>{item.recommendationText}</Text>
|
||||
{/* Summary */}
|
||||
<View style={styles.cardSummary}>
|
||||
<Text
|
||||
style={styles.summaryText}
|
||||
numberOfLines={expanded ? undefined : 3}
|
||||
>
|
||||
{recommendation.recommendationText}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{item.activityPlan && (
|
||||
<>
|
||||
<Text style={styles.sectionTitle}>Activity Plan</Text>
|
||||
<Text style={styles.content}>{item.activityPlan}</Text>
|
||||
</>
|
||||
)}
|
||||
{/* 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>
|
||||
|
||||
{item.dietPlan && (
|
||||
<>
|
||||
<Text style={styles.sectionTitle}>Diet Plan</Text>
|
||||
<Text style={styles.content}>{item.dietPlan}</Text>
|
||||
</>
|
||||
)}
|
||||
</LinearGradient>
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
@@ -196,10 +287,23 @@ const styles = StyleSheet.create({
|
||||
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,
|
||||
paddingHorizontal: 24,
|
||||
marginBottom: 20,
|
||||
borderBottomLeftRadius: theme.borderRadius.xl,
|
||||
borderBottomRightRadius: theme.borderRadius.xl,
|
||||
},
|
||||
@@ -208,112 +312,126 @@ const styles = StyleSheet.create({
|
||||
fontWeight: theme.typography.fontWeight.bold,
|
||||
color: theme.colors.white,
|
||||
},
|
||||
infoBanner: {
|
||||
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",
|
||||
marginHorizontal: 16,
|
||||
marginTop: 16,
|
||||
marginBottom: 12,
|
||||
padding: 14,
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(59, 130, 246, 0.2)",
|
||||
gap: 10,
|
||||
},
|
||||
infoBannerIconContainer: {
|
||||
marginRight: 4,
|
||||
},
|
||||
infoBannerIcon: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
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",
|
||||
},
|
||||
infoBannerText: {
|
||||
flex: 1,
|
||||
fontSize: theme.typography.fontSize.sm,
|
||||
emptyTitle: {
|
||||
fontSize: theme.typography.fontSize.xl,
|
||||
fontWeight: theme.typography.fontWeight.bold,
|
||||
color: theme.colors.gray700,
|
||||
lineHeight: 18,
|
||||
fontWeight: theme.typography.fontWeight.medium,
|
||||
marginTop: 16,
|
||||
marginBottom: 8,
|
||||
},
|
||||
centered: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: theme.colors.background,
|
||||
},
|
||||
listContent: {
|
||||
padding: 16,
|
||||
emptyText: {
|
||||
fontSize: theme.typography.fontSize.base,
|
||||
color: theme.colors.gray500,
|
||||
textAlign: "center",
|
||||
lineHeight: 24,
|
||||
},
|
||||
card: {
|
||||
padding: 18,
|
||||
marginBottom: 14,
|
||||
borderRadius: theme.borderRadius.xl,
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(59, 130, 246, 0.1)",
|
||||
marginBottom: 16,
|
||||
},
|
||||
cardContent: {
|
||||
borderRadius: theme.borderRadius["2xl"],
|
||||
padding: 20,
|
||||
},
|
||||
cardHeader: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 14,
|
||||
paddingBottom: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.colors.gray200,
|
||||
},
|
||||
statusBadge: {
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 5,
|
||||
borderRadius: theme.borderRadius.md,
|
||||
},
|
||||
statusText: {
|
||||
fontSize: theme.typography.fontSize.xs,
|
||||
fontWeight: theme.typography.fontWeight.semibold,
|
||||
color: theme.colors.white,
|
||||
},
|
||||
date: {
|
||||
fontSize: theme.typography.fontSize.xs,
|
||||
color: theme.colors.gray600,
|
||||
fontWeight: theme.typography.fontWeight.medium,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: theme.typography.fontSize.base,
|
||||
fontWeight: theme.typography.fontWeight.semibold,
|
||||
color: theme.colors.gray900,
|
||||
marginTop: 12,
|
||||
marginBottom: 6,
|
||||
},
|
||||
content: {
|
||||
fontSize: theme.typography.fontSize.sm,
|
||||
color: theme.colors.gray700,
|
||||
lineHeight: 20,
|
||||
},
|
||||
emptyContainer: {
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingVertical: 60,
|
||||
},
|
||||
emptyIconContainer: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
emptyIconGradient: {
|
||||
width: 96,
|
||||
height: 96,
|
||||
borderRadius: 48,
|
||||
cardHeaderLeft: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
},
|
||||
cardIcon: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
empty: {
|
||||
textAlign: "center",
|
||||
fontSize: theme.typography.fontSize.base,
|
||||
color: theme.colors.gray700,
|
||||
fontWeight: theme.typography.fontWeight.semibold,
|
||||
marginBottom: 4,
|
||||
cardTitle: {
|
||||
fontSize: theme.typography.fontSize.lg,
|
||||
fontWeight: theme.typography.fontWeight.bold,
|
||||
color: theme.colors.gray800,
|
||||
},
|
||||
emptySub: {
|
||||
textAlign: "center",
|
||||
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,
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user