phase 1-6 implemented

This commit is contained in:
dimitar committed 2026-03-10 04:14:03 +01:00
1 parent 22c274bb83
commit 2c4927478a
73 files changed
+9702 -2354

No files matched your search

+121 -85
View File
@@ -1,18 +1,28 @@
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, ScrollView, Alert } from 'react-native'
import { useState, useEffect, useRef } from 'react'
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 { theme } from '../../styles/theme'
import { Animated } from 'react-native'
import {
View,
Text,
StyleSheet,
TouchableOpacity,
ActivityIndicator,
ScrollView,
Alert,
} from "react-native";
import { useState, useEffect, useRef } from "react";
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 { theme } from "../../styles/theme";
import { Animated } from "react-native";
import { getErrorMessage } from "../../utils/error-helpers";
import log from "../../utils/logger";
export default function AttendanceScreen() {
const { getToken, userId } = useAuth()
const [loading, setLoading] = useState(true)
const [activeCheckIn, setActiveCheckIn] = useState<Attendance | null>(null)
const [history, setHistory] = useState<Attendance[]>([])
const pulseAnim = useRef(new Animated.Value(1)).current
const { getToken, userId } = useAuth();
const [loading, setLoading] = useState(true);
const [activeCheckIn, setActiveCheckIn] = useState<Attendance | null>(null);
const [history, setHistory] = useState<Attendance[]>([]);
const pulseAnim = useRef(new Animated.Value(1)).current;
useEffect(() => {
if (activeCheckIn) {
@@ -28,75 +38,75 @@ export default function AttendanceScreen() {
duration: 1000,
useNativeDriver: true,
}),
])
)
pulse.start()
return () => pulse.stop()
]),
);
pulse.start();
return () => pulse.stop();
}
}, [activeCheckIn])
}, [activeCheckIn]);
const fetchAttendance = async () => {
try {
setLoading(true)
const token = await getToken()
if (!token) return
setLoading(true);
const token = await getToken();
if (!token) return;
console.log('Fetching attendance history')
const data = await attendanceApi.getHistory(token)
setHistory(data)
log.debug("Fetching attendance history");
const data = await attendanceApi.getHistory(token);
setHistory(data);
// Check if there's an active check-in (latest one has no checkOutTime)
if (data.length > 0 && !data[0].checkOutTime) {
setActiveCheckIn(data[0])
setActiveCheckIn(data[0]);
} else {
setActiveCheckIn(null)
setActiveCheckIn(null);
}
} catch (error) {
console.error('Error fetching attendance:', error)
Alert.alert('Error', 'Failed to load attendance data')
log.error("Failed to fetch attendance", error);
Alert.alert("Error", "Failed to load attendance data");
} finally {
setLoading(false)
setLoading(false);
}
}
};
useEffect(() => {
fetchAttendance()
}, [])
fetchAttendance();
}, []);
const handleCheckIn = async () => {
try {
const token = await getToken()
if (!token) return
const token = await getToken();
if (!token) return;
await attendanceApi.checkIn('gym', token)
fetchAttendance()
Alert.alert('Success', 'Checked in successfully!')
} catch (error: any) {
console.error('Check-in error:', error)
Alert.alert('Error', error.response?.data || 'Failed to check in')
await attendanceApi.checkIn("gym", token);
fetchAttendance();
Alert.alert("Success", "Checked in successfully!");
} catch (error: unknown) {
log.error("Failed to check in", error);
Alert.alert("Error", getErrorMessage(error, "Failed to check in"));
}
}
};
const handleCheckOut = async () => {
try {
const token = await getToken()
if (!token) return
const token = await getToken();
if (!token) return;
await attendanceApi.checkOut(token)
fetchAttendance()
Alert.alert('Success', 'Checked out successfully!')
} catch (error: any) {
console.error('Check-out error:', error)
Alert.alert('Error', error.response?.data || 'Failed to check out')
await attendanceApi.checkOut(token);
fetchAttendance();
Alert.alert("Success", "Checked out successfully!");
} catch (error: unknown) {
log.error("Failed to check out", error);
Alert.alert("Error", getErrorMessage(error, "Failed to check out"));
}
}
};
if (loading && !history.length) {
return (
<View style={styles.centered}>
<ActivityIndicator size="large" color={theme.colors.primary} />
</View>
)
);
}
return (
@@ -114,7 +124,7 @@ export default function AttendanceScreen() {
<View style={styles.actionContainer}>
{activeCheckIn ? (
<LinearGradient
colors={['rgba(16, 185, 129, 0.15)', 'rgba(5, 150, 105, 0.1)']}
colors={["rgba(16, 185, 129, 0.15)", "rgba(5, 150, 105, 0.1)"]}
style={[styles.activeCard, theme.shadows.medium]}
>
<View style={styles.activeCardContent}>
@@ -129,7 +139,11 @@ export default function AttendanceScreen() {
<View style={styles.activeTextContainer}>
<Text style={styles.activeText}>Currently Checked In</Text>
<Text style={styles.timeText}>
Since {new Date(activeCheckIn.checkInTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
Since{" "}
{new Date(activeCheckIn.checkInTime).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</Text>
</View>
</View>
@@ -140,7 +154,12 @@ export default function AttendanceScreen() {
end={{ x: 1, y: 0 }}
style={[styles.checkOutButton, theme.shadows.medium]}
>
<Ionicons name="log-out-outline" size={20} color="#fff" style={{ marginRight: 8 }} />
<Ionicons
name="log-out-outline"
size={20}
color="#fff"
style={{ marginRight: 8 }}
/>
<Text style={styles.buttonText}>Check Out</Text>
</LinearGradient>
</TouchableOpacity>
@@ -154,7 +173,12 @@ export default function AttendanceScreen() {
end={{ x: 1, y: 0 }}
style={[styles.checkInButton, theme.shadows.glow]}
>
<Ionicons name="log-in-outline" size={24} color="#fff" style={{ marginRight: 8 }} />
<Ionicons
name="log-in-outline"
size={24}
color="#fff"
style={{ marginRight: 8 }}
/>
<Text style={styles.checkInButtonText}>Check In</Text>
</LinearGradient>
</TouchableOpacity>
@@ -166,13 +190,17 @@ export default function AttendanceScreen() {
{history.map((item) => (
<LinearGradient
key={item.id}
colors={['rgba(255, 255, 255, 1)', 'rgba(249, 250, 251, 1)'] as const}
colors={["rgba(255, 255, 255, 1)", "rgba(249, 250, 251, 1)"] as const}
style={[styles.historyItem, theme.shadows.medium]}
>
<View style={styles.historyLeft}>
<View style={styles.historyIconContainer}>
<LinearGradient
colors={item.checkOutTime ? theme.gradients.success : theme.gradients.primary}
colors={
item.checkOutTime
? theme.gradients.success
: theme.gradients.primary
}
style={styles.historyIcon}
>
<Ionicons
@@ -191,18 +219,26 @@ export default function AttendanceScreen() {
</View>
<View style={styles.timeContainer}>
<Text style={styles.historyTime}>
In: {new Date(item.checkInTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
In:{" "}
{new Date(item.checkInTime).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</Text>
{item.checkOutTime && (
<Text style={styles.historyTime}>
Out: {new Date(item.checkOutTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
Out:{" "}
{new Date(item.checkOutTime).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</Text>
)}
</View>
</LinearGradient>
))}
</ScrollView>
)
);
}
const styles = StyleSheet.create({
@@ -215,8 +251,8 @@ const styles = StyleSheet.create({
},
centered: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
justifyContent: "center",
alignItems: "center",
},
header: {
paddingTop: 60,
@@ -227,14 +263,14 @@ const styles = StyleSheet.create({
borderBottomRightRadius: theme.borderRadius.xl,
},
title: {
fontSize: theme.typography.fontSize['3xl'],
fontSize: theme.typography.fontSize["3xl"],
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.white,
marginBottom: 4,
},
subtitle: {
fontSize: theme.typography.fontSize.base,
color: 'rgba(255, 255, 255, 0.9)',
color: "rgba(255, 255, 255, 0.9)",
},
actionContainer: {
marginBottom: 32,
@@ -244,9 +280,9 @@ const styles = StyleSheet.create({
paddingVertical: 20,
paddingHorizontal: 24,
borderRadius: theme.borderRadius.xl,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
},
checkInButtonText: {
color: theme.colors.white,
@@ -257,9 +293,9 @@ const styles = StyleSheet.create({
paddingVertical: 14,
paddingHorizontal: 20,
borderRadius: theme.borderRadius.lg,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
marginTop: 16,
},
buttonText: {
@@ -271,11 +307,11 @@ const styles = StyleSheet.create({
padding: 20,
borderRadius: theme.borderRadius.xl,
borderWidth: 1,
borderColor: 'rgba(16, 185, 129, 0.2)',
borderColor: "rgba(16, 185, 129, 0.2)",
},
activeCardContent: {
flexDirection: 'row',
alignItems: 'center',
flexDirection: "row",
alignItems: "center",
marginBottom: 16,
},
activeIconContainer: {
@@ -285,8 +321,8 @@ const styles = StyleSheet.create({
width: 56,
height: 56,
borderRadius: 28,
justifyContent: 'center',
alignItems: 'center',
justifyContent: "center",
alignItems: "center",
},
activeTextContainer: {
flex: 1,
@@ -313,15 +349,15 @@ const styles = StyleSheet.create({
borderRadius: theme.borderRadius.xl,
marginBottom: 12,
marginHorizontal: 20,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
borderWidth: 1,
borderColor: 'rgba(59, 130, 246, 0.1)',
borderColor: "rgba(59, 130, 246, 0.1)",
},
historyLeft: {
flexDirection: 'row',
alignItems: 'center',
flexDirection: "row",
alignItems: "center",
gap: 12,
},
historyIconContainer: {
@@ -331,8 +367,8 @@ const styles = StyleSheet.create({
width: 32,
height: 32,
borderRadius: 16,
justifyContent: 'center',
alignItems: 'center',
justifyContent: "center",
alignItems: "center",
},
dateText: {
fontSize: theme.typography.fontSize.base,
@@ -345,10 +381,10 @@ const styles = StyleSheet.create({
marginTop: 2,
},
timeContainer: {
alignItems: 'flex-end',
alignItems: "flex-end",
},
historyTime: {
fontSize: theme.typography.fontSize.sm,
color: theme.colors.gray700,
},
})
});
+13 -13
View File
@@ -22,6 +22,7 @@ import {
} from "../../services/fitnessGoals";
import { useFocusEffect } from "expo-router";
import * as SecureStore from "expo-secure-store";
import log from "../../utils/logger";
export default function GoalsScreen() {
const { user } = useUser();
@@ -35,11 +36,11 @@ export default function GoalsScreen() {
if (!user?.id) return;
try {
const token = await getToken();
console.log(
"Token obtained:",
token ? "Yes (" + token.substring(0, 20) + "...)" : "No",
);
console.log("User ID:", user.id);
log.debug("Token obtained", {
hasToken: !!token,
tokenPreview: token ? token.substring(0, 20) + "..." : "No",
userId: user.id,
});
// Decode and log token details for debugging
if (token) {
@@ -47,21 +48,20 @@ export default function GoalsScreen() {
const parts = token.split(".");
if (parts.length === 3) {
const payload = JSON.parse(atob(parts[1]));
console.log("Token issuer:", payload.iss);
console.log(
"Token kid from header:",
JSON.parse(atob(parts[0])).kid,
);
log.debug("Token details", {
issuer: payload.iss,
kid: JSON.parse(atob(parts[0])).kid,
});
}
} catch (e) {
console.log("Could not decode token");
log.warn("Could not decode token");
}
}
const loadedGoals = await fitnessGoalsService.getGoals(user.id, token);
setGoals(loadedGoals);
} catch (error) {
console.error("Error loading goals:", error);
log.error("Failed to load goals", error);
}
}, [user?.id, getToken]);
@@ -99,7 +99,7 @@ export default function GoalsScreen() {
"Cache cleared! Please sign out and sign back in.",
);
} catch (error) {
console.error("Error clearing cache:", error);
log.error("Failed to clear cache", error);
Alert.alert("Error", "Failed to clear cache");
}
},
+35 -36
View File
@@ -17,6 +17,7 @@ import { AnimatedButton } from "../../components/AnimatedButton";
import { GradientBackground } from "../../components/GradientBackground";
import { useState, useEffect } from "react";
import { API_BASE_URL, API_ENDPOINTS } from "../../config/api";
import log from "../../utils/logger";
export default function ProfileScreen() {
const { user } = useUser();
@@ -27,7 +28,7 @@ export default function ProfileScreen() {
try {
await signOut();
} catch (err) {
console.error("Error signing out:", err);
log.error("Failed to sign out", err);
}
};
@@ -69,26 +70,32 @@ export default function ProfileScreen() {
setGymsLoading(true);
const token = await getToken();
const url = `${API_BASE_URL}${API_ENDPOINTS.GYMS}`;
console.log("Profile.loadGyms fetching:", url);
log.debug("Loading gyms", { url });
const res = await fetch(url, {
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
});
const contentType = res.headers.get("content-type") || "";
if (!res.ok) {
const text = await res.text().catch(() => "");
console.error("Failed to fetch gyms: non-OK response", {
status: res.status,
body: text?.slice(0, 200),
});
log.error(
"Failed to fetch gyms - non-OK response",
new Error(text.slice(0, 200)),
{
status: res.status,
},
);
setGyms([]);
return;
}
if (!contentType.includes("application/json")) {
const text = await res.text().catch(() => "");
console.error("Failed to fetch gyms: expected JSON", {
contentType,
body: text?.slice(0, 200),
});
log.error(
"Failed to fetch gyms - expected JSON",
new Error(text.slice(0, 200)),
{
contentType,
},
);
setGyms([]);
return;
}
@@ -97,12 +104,9 @@ export default function ProfileScreen() {
data = await res.json();
} catch (e) {
const text = await res.text().catch(() => "");
console.error(
"Failed to parse gyms JSON:",
e,
"body:",
text?.slice(0, 200),
);
log.error("Failed to parse gyms JSON", e, {
bodyPreview: text?.slice(0, 200),
});
setGyms([]);
return;
}
@@ -119,7 +123,7 @@ export default function ProfileScreen() {
if (selectedGymId === null) setSelectedGymId(gid);
}
} catch (err) {
console.error("Failed to fetch gyms:", err);
log.error("Failed to fetch gyms", err);
setGyms([]);
} finally {
setGymsLoading(false);
@@ -130,12 +134,7 @@ export default function ProfileScreen() {
try {
const token = await getToken();
const url = `${API_BASE_URL}${API_ENDPOINTS.USERS}/gym`;
console.log(
"Profile.handleApplyGym PATCH:",
url,
"gymId:",
selectedGymId,
);
log.debug("Updating gym selection", { url, gymId: selectedGymId });
const res = await fetch(url, {
method: "PATCH",
headers: {
@@ -147,25 +146,25 @@ export default function ProfileScreen() {
const contentType = res.headers.get("content-type") || "";
if (!res.ok) {
const text = await res.text().catch(() => "");
console.error("Failed to update gym selection: non-OK response", {
status: res.status,
body: text?.slice(0, 200),
});
log.error(
"Failed to update gym selection - non-OK response",
new Error(text.slice(0, 200)),
{
status: res.status,
},
);
Alert.alert("Error", "Failed to update gym selection");
return;
}
if (contentType.includes("application/json")) {
try {
const data = await res.json();
console.log("Gym selection updated:", data);
log.debug("Gym selection updated", { data });
} catch (e) {
const text = await res.text().catch(() => "");
console.error(
"Failed to parse update response JSON:",
e,
"body:",
text?.slice(0, 200),
);
log.error("Failed to parse update response JSON", e, {
bodyPreview: text?.slice(0, 200),
});
}
}
// Update current gym state for immediate UI reflection
@@ -179,14 +178,14 @@ export default function ProfileScreen() {
try {
await (user as any)?.reload?.();
} catch (e) {
console.log("Profile.handleApplyGym: failed to reload user", e);
log.debug("Failed to reload user after gym update", { error: e });
}
Alert.alert(
"Success",
selectedGymId ? "Gym selected successfully" : "Proceeding without gym",
);
} catch (err) {
console.error("Failed to update gym selection:", err);
log.error("Failed to update gym selection", err);
Alert.alert("Error", "Failed to update gym selection");
}
};
+293 -265
View File
@@ -1,291 +1,319 @@
import { useEffect, useState } from "react";
import { View, Text, FlatList, ActivityIndicator, StyleSheet, RefreshControl } from "react-native";
import {
View,
Text,
FlatList,
ActivityIndicator,
StyleSheet,
RefreshControl,
} 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 { theme } from "../../styles/theme";
import log from "../../utils/logger";
interface Recommendation {
id: string;
userId: string;
recommendationText: string;
activityPlan?: string;
dietPlan?: string;
status: string;
createdAt: string;
id: string;
userId: string;
recommendationText: string;
activityPlan?: string;
dietPlan?: string;
status: string;
createdAt: string;
}
export default function RecommendationsScreen() {
const { getToken, userId } = useAuth();
const [recommendations, setRecommendations] = useState<Recommendation[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const { getToken, userId } = useAuth();
const [recommendations, setRecommendations] = useState<Recommendation[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const fetchRecommendations = async () => {
try {
if (!userId) {
console.error('No userId available');
return;
}
const fetchRecommendations = async () => {
try {
if (!userId) {
log.warn("No userId available");
return;
}
const token = await getToken();
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
const token = await getToken();
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
if (token) {
headers["Authorization"] = `Bearer ${token}`;
}
const url = `${API_BASE_URL}${API_ENDPOINTS.RECOMMENDATIONS}?userId=${userId}`;
console.log('Fetching recommendations from:', url);
const url = `${API_BASE_URL}${API_ENDPOINTS.RECOMMENDATIONS}?userId=${userId}`;
log.apiRequest("GET", url);
const res = await fetch(url, { headers });
const res = await fetch(url, { headers });
if (!res.ok) {
const errorText = await res.text();
console.error('API Error:', res.status, errorText);
throw new Error(`Network response was not ok: ${res.status}`);
}
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();
console.log('Recommendations data:', data);
setRecommendations(data.recommendations || data || []);
} catch (e) {
console.error('Failed to load recommendations', e);
} finally {
setLoading(false);
setRefreshing(false);
}
};
useEffect(() => {
fetchRecommendations();
}, []);
const onRefresh = () => {
setRefreshing(true);
fetchRecommendations();
};
if (loading) {
return (
<View style={styles.centered}>
<ActivityIndicator size="large" color={theme.colors.primary} />
</View>
);
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);
} finally {
setLoading(false);
setRefreshing(false);
}
};
useEffect(() => {
fetchRecommendations();
}, []);
const onRefresh = () => {
setRefreshing(true);
fetchRecommendations();
};
if (loading) {
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}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
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>
</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.cardHeader}>
<LinearGradient
colors={theme.gradients.success}
style={styles.statusBadge}
>
<Text style={styles.statusText}>{item.status.toUpperCase()}</Text>
</LinearGradient>
<Text style={styles.date}>{new Date(item.createdAt).toLocaleDateString()}</Text>
</View>
<Text style={styles.sectionTitle}>Daily Advice</Text>
<Text style={styles.content}>{item.recommendationText}</Text>
{item.activityPlan && (
<>
<Text style={styles.sectionTitle}>Activity Plan</Text>
<Text style={styles.content}>{item.activityPlan}</Text>
</>
)}
{item.dietPlan && (
<>
<Text style={styles.sectionTitle}>Diet Plan</Text>
<Text style={styles.content}>{item.dietPlan}</Text>
</>
)}
</LinearGradient>
)}
/>
</View>
<View style={styles.centered}>
<ActivityIndicator size="large" color={theme.colors.primary} />
</View>
);
}
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}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
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>
</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.cardHeader}>
<LinearGradient
colors={theme.gradients.success}
style={styles.statusBadge}
>
<Text style={styles.statusText}>
{item.status.toUpperCase()}
</Text>
</LinearGradient>
<Text style={styles.date}>
{new Date(item.createdAt).toLocaleDateString()}
</Text>
</View>
<Text style={styles.sectionTitle}>Daily Advice</Text>
<Text style={styles.content}>{item.recommendationText}</Text>
{item.activityPlan && (
<>
<Text style={styles.sectionTitle}>Activity Plan</Text>
<Text style={styles.content}>{item.activityPlan}</Text>
</>
)}
{item.dietPlan && (
<>
<Text style={styles.sectionTitle}>Diet Plan</Text>
<Text style={styles.content}>{item.dietPlan}</Text>
</>
)}
</LinearGradient>
)}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
header: {
paddingTop: 60,
paddingBottom: 24,
paddingHorizontal: 24,
borderBottomLeftRadius: theme.borderRadius.xl,
borderBottomRightRadius: theme.borderRadius.xl,
},
headerTitle: {
fontSize: theme.typography.fontSize['3xl'],
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.white,
},
infoBanner: {
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',
alignItems: 'center',
},
infoBannerText: {
flex: 1,
fontSize: theme.typography.fontSize.sm,
color: theme.colors.gray700,
lineHeight: 18,
fontWeight: theme.typography.fontWeight.medium,
},
centered: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: theme.colors.background,
},
listContent: {
padding: 16,
},
card: {
padding: 18,
marginBottom: 14,
borderRadius: theme.borderRadius.xl,
borderWidth: 1,
borderColor: 'rgba(59, 130, 246, 0.1)',
},
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,
justifyContent: 'center',
alignItems: 'center',
},
empty: {
textAlign: 'center',
fontSize: theme.typography.fontSize.base,
color: theme.colors.gray700,
fontWeight: theme.typography.fontWeight.semibold,
marginBottom: 4,
},
emptySub: {
textAlign: 'center',
fontSize: theme.typography.fontSize.sm,
color: theme.colors.gray500,
},
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
header: {
paddingTop: 60,
paddingBottom: 24,
paddingHorizontal: 24,
borderBottomLeftRadius: theme.borderRadius.xl,
borderBottomRightRadius: theme.borderRadius.xl,
},
headerTitle: {
fontSize: theme.typography.fontSize["3xl"],
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.white,
},
infoBanner: {
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",
alignItems: "center",
},
infoBannerText: {
flex: 1,
fontSize: theme.typography.fontSize.sm,
color: theme.colors.gray700,
lineHeight: 18,
fontWeight: theme.typography.fontWeight.medium,
},
centered: {
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: theme.colors.background,
},
listContent: {
padding: 16,
},
card: {
padding: 18,
marginBottom: 14,
borderRadius: theme.borderRadius.xl,
borderWidth: 1,
borderColor: "rgba(59, 130, 246, 0.1)",
},
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,
justifyContent: "center",
alignItems: "center",
},
empty: {
textAlign: "center",
fontSize: theme.typography.fontSize.base,
color: theme.colors.gray700,
fontWeight: theme.typography.fontWeight.semibold,
marginBottom: 4,
},
emptySub: {
textAlign: "center",
fontSize: theme.typography.fontSize.sm,
color: theme.colors.gray500,
},
});