3 and 4 completed

This commit is contained in:
dimitar committed 2026-03-11 02:38:43 +01:00
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
+84
View File
@@ -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,
+34 -32
View File
@@ -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}>
+346 -228
View File
@@ -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,
},
});