779 lines
24 KiB
TypeScript
779 lines
24 KiB
TypeScript
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
ScrollView,
|
|
RefreshControl,
|
|
Image,
|
|
Animated,
|
|
TouchableOpacity,
|
|
} from "react-native";
|
|
import { useUser } from "@clerk/clerk-expo";
|
|
import { useState, useCallback, useEffect, useRef, useMemo } from "react";
|
|
import { useFocusEffect } from "@react-navigation/native";
|
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
import { useTheme } from "../../contexts/ThemeContext";
|
|
import { useStatistics } from "../../contexts/StatisticsContext";
|
|
import { useFitnessGoals } from "../../contexts/FitnessGoalsContext";
|
|
import { MinimalCard } from "../../components/MinimalCard";
|
|
import { SectionHeader } from "../../components/SectionHeader";
|
|
import { IconContainer } from "../../components/IconContainer";
|
|
import { ProgressBar } from "../../components/ProgressBar";
|
|
import { TrackMealModal } from "../../components/TrackMealModal";
|
|
import { AddWaterModal } from "../../components/AddWaterModal";
|
|
import { ScanFoodModal } from "../../components/ScanFoodModal";
|
|
import { ActivityRing } from "../../components/ActivityRing";
|
|
import {
|
|
checkInsToActivities,
|
|
completedGoalsToActivities,
|
|
newGoalsToActivities,
|
|
combineActivities,
|
|
getRecentActivities,
|
|
formatActivityTime,
|
|
formatDuration,
|
|
getActivityIcon,
|
|
getActivityEmoji,
|
|
ActivityType,
|
|
} from "../../utils/activityFeed";
|
|
|
|
const CALORIE_GOAL = 2000;
|
|
const WATER_GOAL = 2000;
|
|
const WORKOUT_GOAL = 3;
|
|
|
|
export default function HomeScreen() {
|
|
const { user } = useUser();
|
|
const { colors, typography } = useTheme();
|
|
const { refetchStatistics, forceRefresh, statistics, loading } =
|
|
useStatistics();
|
|
const { goals, refetchGoals } = useFitnessGoals();
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const [trackMealModalVisible, setTrackMealModalVisible] = useState(false);
|
|
const [addWaterModalVisible, setAddWaterModalVisible] = useState(false);
|
|
const [scanFoodModalVisible, setScanFoodModalVisible] = useState(false);
|
|
const [calories, setCalories] = useState(0);
|
|
const [waterIntake, setWaterIntake] = useState(0);
|
|
|
|
const caloriesBounce = useRef(new Animated.Value(1)).current;
|
|
const waterBounce = useRef(new Animated.Value(1)).current;
|
|
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
refetchStatistics();
|
|
refetchGoals();
|
|
}, [refetchStatistics, refetchGoals]),
|
|
);
|
|
|
|
const onRefresh = useCallback(async () => {
|
|
setRefreshing(true);
|
|
await Promise.all([forceRefresh(), refetchGoals()]);
|
|
setRefreshing(false);
|
|
}, [forceRefresh, refetchGoals]);
|
|
|
|
const getGreeting = () => {
|
|
const hour = new Date().getHours();
|
|
if (hour < 12) return "Good Morning";
|
|
if (hour < 18) return "Good Afternoon";
|
|
return "Good Evening";
|
|
};
|
|
|
|
const getMotivationalMessage = () => {
|
|
const messages = [
|
|
"Let's crush it today! 💪",
|
|
"Ready to level up? 🔥",
|
|
"You've got this! ⚡",
|
|
"Time to shine! ✨",
|
|
"Let's make it happen! 🚀",
|
|
];
|
|
return messages[Math.floor(Math.random() * messages.length)];
|
|
};
|
|
|
|
const handleSaveMeal = (meal: {
|
|
type: string;
|
|
name: string;
|
|
calories: number;
|
|
}) => {
|
|
setCalories((prev) => prev + meal.calories);
|
|
setTrackMealModalVisible(false);
|
|
Animated.sequence([
|
|
Animated.timing(caloriesBounce, {
|
|
toValue: 1.15,
|
|
duration: 150,
|
|
useNativeDriver: true,
|
|
}),
|
|
Animated.timing(caloriesBounce, {
|
|
toValue: 1,
|
|
duration: 150,
|
|
useNativeDriver: true,
|
|
}),
|
|
]).start();
|
|
};
|
|
|
|
const handleAddWater = (amount: number) => {
|
|
setWaterIntake((prev) => prev + amount);
|
|
setAddWaterModalVisible(false);
|
|
Animated.sequence([
|
|
Animated.timing(waterBounce, {
|
|
toValue: 1.15,
|
|
duration: 150,
|
|
useNativeDriver: true,
|
|
}),
|
|
Animated.timing(waterBounce, {
|
|
toValue: 1,
|
|
duration: 150,
|
|
useNativeDriver: true,
|
|
}),
|
|
]).start();
|
|
};
|
|
|
|
const handleResetCalories = () => setCalories(0);
|
|
const handleResetWater = () => setWaterIntake(0);
|
|
const handleAddScannedFood = (scannedCalories: number) => {
|
|
setCalories((prev) => prev + scannedCalories);
|
|
setScanFoodModalVisible(false);
|
|
};
|
|
|
|
const resetAllCounters = async () => {
|
|
setCalories(0);
|
|
setWaterIntake(0);
|
|
const today = new Date().toDateString();
|
|
await AsyncStorage.setItem("lastResetDate", today);
|
|
await AsyncStorage.removeItem(`calories_${today}`);
|
|
await AsyncStorage.removeItem(`water_${today}`);
|
|
};
|
|
|
|
useEffect(() => {
|
|
const loadPersistedData = async () => {
|
|
const today = new Date().toDateString();
|
|
const storedCalories = await AsyncStorage.getItem(`calories_${today}`);
|
|
const storedWater = await AsyncStorage.getItem(`water_${today}`);
|
|
if (storedCalories) setCalories(parseInt(storedCalories, 10));
|
|
if (storedWater) setWaterIntake(parseInt(storedWater, 10));
|
|
};
|
|
loadPersistedData();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const persistCalories = async () => {
|
|
const today = new Date().toDateString();
|
|
await AsyncStorage.setItem(`calories_${today}`, calories.toString());
|
|
};
|
|
persistCalories();
|
|
}, [calories]);
|
|
|
|
useEffect(() => {
|
|
const persistWater = async () => {
|
|
const today = new Date().toDateString();
|
|
await AsyncStorage.setItem(`water_${today}`, waterIntake.toString());
|
|
};
|
|
persistWater();
|
|
}, [waterIntake]);
|
|
|
|
useEffect(() => {
|
|
const checkAndResetIfNeeded = async () => {
|
|
const lastResetDate = await AsyncStorage.getItem("lastResetDate");
|
|
const today = new Date().toDateString();
|
|
if (lastResetDate !== today) {
|
|
await resetAllCounters();
|
|
}
|
|
};
|
|
checkAndResetIfNeeded();
|
|
}, []);
|
|
|
|
const checkInsThisWeek = statistics?.attendance.checkInsThisWeek || 0;
|
|
const currentStreak = statistics?.attendance.currentStreak || 0;
|
|
const workoutsThisWeek = checkInsThisWeek;
|
|
|
|
const recentActivities = useMemo(() => {
|
|
if (!statistics || !goals) return [];
|
|
const checkInActivities = checkInsToActivities(statistics.attendance).slice(
|
|
0,
|
|
2,
|
|
);
|
|
const completedGoalActivities = completedGoalsToActivities(goals).slice(
|
|
0,
|
|
2,
|
|
);
|
|
const newGoalActivities = newGoalsToActivities(goals, 7);
|
|
const allActivities = combineActivities(
|
|
checkInActivities,
|
|
completedGoalActivities,
|
|
newGoalActivities,
|
|
);
|
|
return getRecentActivities(allActivities, 5);
|
|
}, [statistics, goals]);
|
|
|
|
return (
|
|
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
|
<ScrollView
|
|
contentContainerStyle={styles.scrollContent}
|
|
refreshControl={
|
|
<RefreshControl
|
|
refreshing={refreshing}
|
|
onRefresh={onRefresh}
|
|
tintColor={colors.primary}
|
|
/>
|
|
}
|
|
>
|
|
{/* Hero Header */}
|
|
<View style={styles.heroHeader}>
|
|
<View style={styles.heroTextContainer}>
|
|
<Text
|
|
style={[
|
|
typography.label,
|
|
{ color: colors.primary, marginBottom: 4 },
|
|
]}
|
|
>
|
|
{getGreeting()} 👋
|
|
</Text>
|
|
<Text
|
|
style={[
|
|
typography.h1,
|
|
{ color: colors.textPrimary, marginBottom: 4 },
|
|
]}
|
|
>
|
|
{user?.firstName || "Champion"}
|
|
</Text>
|
|
<Text style={[typography.body, { color: colors.textSecondary }]}>
|
|
{getMotivationalMessage()}
|
|
</Text>
|
|
</View>
|
|
<TouchableOpacity activeOpacity={0.8}>
|
|
{user?.imageUrl ? (
|
|
<Image
|
|
source={{ uri: user.imageUrl }}
|
|
style={styles.heroAvatar}
|
|
/>
|
|
) : (
|
|
<View
|
|
style={[
|
|
styles.heroPlaceholderAvatar,
|
|
{ backgroundColor: colors.primary },
|
|
]}
|
|
>
|
|
<Ionicons name="person" size={28} color={colors.white} />
|
|
</View>
|
|
)}
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Activity Rings - Hero Section */}
|
|
<View style={styles.section}>
|
|
<MinimalCard variant="elevated" style={styles.ringsCard}>
|
|
<View style={styles.ringsContainer}>
|
|
<ActivityRing
|
|
progress={(calories / CALORIE_GOAL) * 100}
|
|
current={calories}
|
|
goal={CALORIE_GOAL}
|
|
label="CALORIES"
|
|
color={colors.calories}
|
|
size={95}
|
|
strokeWidth={9}
|
|
icon={
|
|
<Text
|
|
style={[
|
|
typography.statLarge,
|
|
{ color: colors.calories, fontSize: 24 },
|
|
]}
|
|
>
|
|
{calories}
|
|
</Text>
|
|
}
|
|
/>
|
|
<ActivityRing
|
|
progress={(waterIntake / WATER_GOAL) * 100}
|
|
current={waterIntake}
|
|
goal={WATER_GOAL}
|
|
label="WATER (ML)"
|
|
color={colors.water}
|
|
size={95}
|
|
strokeWidth={9}
|
|
icon={
|
|
<Text
|
|
style={[
|
|
typography.statLarge,
|
|
{ color: colors.water, fontSize: 24 },
|
|
]}
|
|
>
|
|
{waterIntake}
|
|
</Text>
|
|
}
|
|
/>
|
|
<ActivityRing
|
|
progress={(workoutsThisWeek / WORKOUT_GOAL) * 100}
|
|
current={workoutsThisWeek}
|
|
goal={WORKOUT_GOAL}
|
|
label="WORKOUTS"
|
|
color={colors.workouts}
|
|
size={95}
|
|
strokeWidth={9}
|
|
icon={
|
|
<Text
|
|
style={[
|
|
typography.statLarge,
|
|
{ color: colors.workouts, fontSize: 24 },
|
|
]}
|
|
>
|
|
{workoutsThisWeek}
|
|
</Text>
|
|
}
|
|
/>
|
|
</View>
|
|
|
|
{/* Streak Banner */}
|
|
{currentStreak >= 1 && (
|
|
<View
|
|
style={[
|
|
styles.streakBanner,
|
|
{ backgroundColor: `${colors.warning}15` },
|
|
]}
|
|
>
|
|
<Text style={{ fontSize: 20 }}>🔥</Text>
|
|
<Text
|
|
style={[
|
|
typography.bodyEmphasis,
|
|
{ color: colors.warning, marginLeft: 8, flex: 1 },
|
|
]}
|
|
>
|
|
{currentStreak} Day Streak!
|
|
</Text>
|
|
<Text
|
|
style={[typography.caption, { color: colors.textTertiary }]}
|
|
>
|
|
Keep it going
|
|
</Text>
|
|
</View>
|
|
)}
|
|
</MinimalCard>
|
|
</View>
|
|
|
|
{/* Quick Actions */}
|
|
<View style={styles.section}>
|
|
<SectionHeader
|
|
title="Quick Actions"
|
|
subtitle="What do you want to do?"
|
|
/>
|
|
<View style={styles.quickActionsGrid}>
|
|
<TouchableOpacity
|
|
onPress={() => console.log("Log workout")}
|
|
activeOpacity={0.85}
|
|
style={[
|
|
styles.quickActionCard,
|
|
{ backgroundColor: colors.primary },
|
|
]}
|
|
>
|
|
<View
|
|
style={[
|
|
styles.quickActionIcon,
|
|
{ backgroundColor: "rgba(255,255,255,0.2)" },
|
|
]}
|
|
>
|
|
<Ionicons name="barbell" size={28} color={colors.white} />
|
|
</View>
|
|
<Text
|
|
style={[typography.h4, { color: colors.white, marginTop: 12 }]}
|
|
>
|
|
Workout
|
|
</Text>
|
|
<Text
|
|
style={[
|
|
typography.caption,
|
|
{ color: "rgba(255,255,255,0.7)", marginTop: 4 },
|
|
]}
|
|
>
|
|
Log your session
|
|
</Text>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
onPress={() => setTrackMealModalVisible(true)}
|
|
activeOpacity={0.85}
|
|
style={[
|
|
styles.quickActionCard,
|
|
{ backgroundColor: colors.success },
|
|
]}
|
|
>
|
|
<View
|
|
style={[
|
|
styles.quickActionIcon,
|
|
{ backgroundColor: "rgba(255,255,255,0.2)" },
|
|
]}
|
|
>
|
|
<Ionicons name="restaurant" size={28} color={colors.white} />
|
|
</View>
|
|
<Text
|
|
style={[typography.h4, { color: colors.white, marginTop: 12 }]}
|
|
>
|
|
Meal
|
|
</Text>
|
|
<Text
|
|
style={[
|
|
typography.caption,
|
|
{ color: "rgba(255,255,255,0.7)", marginTop: 4 },
|
|
]}
|
|
>
|
|
Track calories
|
|
</Text>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
onPress={() => setAddWaterModalVisible(true)}
|
|
activeOpacity={0.85}
|
|
style={[styles.quickActionCard, { backgroundColor: colors.info }]}
|
|
>
|
|
<View
|
|
style={[
|
|
styles.quickActionIcon,
|
|
{ backgroundColor: "rgba(255,255,255,0.2)" },
|
|
]}
|
|
>
|
|
<Ionicons name="water" size={28} color={colors.white} />
|
|
</View>
|
|
<Text
|
|
style={[typography.h4, { color: colors.white, marginTop: 12 }]}
|
|
>
|
|
Hydrate
|
|
</Text>
|
|
<Text
|
|
style={[
|
|
typography.caption,
|
|
{ color: "rgba(255,255,255,0.7)", marginTop: 4 },
|
|
]}
|
|
>
|
|
Add water
|
|
</Text>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
onPress={() => setScanFoodModalVisible(true)}
|
|
activeOpacity={0.85}
|
|
style={[
|
|
styles.quickActionCard,
|
|
{ backgroundColor: colors.accent },
|
|
]}
|
|
>
|
|
<View
|
|
style={[
|
|
styles.quickActionIcon,
|
|
{ backgroundColor: "rgba(255,255,255,0.2)" },
|
|
]}
|
|
>
|
|
<Ionicons name="scan" size={28} color={colors.white} />
|
|
</View>
|
|
<Text
|
|
style={[typography.h4, { color: colors.white, marginTop: 12 }]}
|
|
>
|
|
Scan
|
|
</Text>
|
|
<Text
|
|
style={[
|
|
typography.caption,
|
|
{ color: "rgba(255,255,255,0.7)", marginTop: 4 },
|
|
]}
|
|
>
|
|
Quick add
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Today's Progress */}
|
|
<View style={styles.section}>
|
|
<SectionHeader
|
|
title="Today's Progress"
|
|
subtitle="Track your daily goals"
|
|
/>
|
|
|
|
<MinimalCard variant="bordered" style={styles.progressCard}>
|
|
<View style={styles.progressHeader}>
|
|
<View style={styles.progressLabelRow}>
|
|
<View
|
|
style={[
|
|
styles.progressIcon,
|
|
{ backgroundColor: `${colors.calories}20` },
|
|
]}
|
|
>
|
|
<Text style={{ fontSize: 20 }}>🔥</Text>
|
|
</View>
|
|
<View style={{ marginLeft: 12 }}>
|
|
<Text style={[typography.h4, { color: colors.textPrimary }]}>
|
|
Calories
|
|
</Text>
|
|
<Text
|
|
style={[typography.caption, { color: colors.textTertiary }]}
|
|
>
|
|
{calories >= CALORIE_GOAL
|
|
? "Goal reached! 🎉"
|
|
: `${CALORIE_GOAL - calories} remaining`}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
<Text style={[typography.h3, { color: colors.calories }]}>
|
|
{calories}/{CALORIE_GOAL}
|
|
</Text>
|
|
</View>
|
|
<ProgressBar
|
|
progress={Math.min(calories / CALORIE_GOAL, 1)}
|
|
color={colors.calories}
|
|
height={12}
|
|
style={{ marginTop: 16 }}
|
|
/>
|
|
</MinimalCard>
|
|
|
|
<MinimalCard
|
|
variant="bordered"
|
|
style={[styles.progressCard, { marginTop: 12 }]}
|
|
>
|
|
<View style={styles.progressHeader}>
|
|
<View style={styles.progressLabelRow}>
|
|
<View
|
|
style={[
|
|
styles.progressIcon,
|
|
{ backgroundColor: `${colors.water}20` },
|
|
]}
|
|
>
|
|
<Text style={{ fontSize: 20 }}>💧</Text>
|
|
</View>
|
|
<View style={{ marginLeft: 12 }}>
|
|
<Text style={[typography.h4, { color: colors.textPrimary }]}>
|
|
Hydration
|
|
</Text>
|
|
<Text
|
|
style={[typography.caption, { color: colors.textTertiary }]}
|
|
>
|
|
{waterIntake >= WATER_GOAL
|
|
? "Fully hydrated! 🌊"
|
|
: `${WATER_GOAL - waterIntake}ml remaining`}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
<Text style={[typography.h3, { color: colors.water }]}>
|
|
{waterIntake}/{WATER_GOAL}
|
|
</Text>
|
|
</View>
|
|
<ProgressBar
|
|
progress={Math.min(waterIntake / WATER_GOAL, 1)}
|
|
color={colors.water}
|
|
height={12}
|
|
style={{ marginTop: 16 }}
|
|
/>
|
|
</MinimalCard>
|
|
</View>
|
|
|
|
{/* Recent Activity */}
|
|
<View style={styles.section}>
|
|
<SectionHeader
|
|
title="Recent Activity"
|
|
subtitle={
|
|
recentActivities.length === 0
|
|
? "No activity yet"
|
|
: `${recentActivities.length} activities`
|
|
}
|
|
/>
|
|
|
|
{recentActivities.length === 0 ? (
|
|
<MinimalCard variant="elevated" style={styles.emptyCard}>
|
|
<Text style={{ fontSize: 48 }}>🏃</Text>
|
|
<Text
|
|
style={[
|
|
typography.h3,
|
|
{
|
|
color: colors.textPrimary,
|
|
marginTop: 16,
|
|
textAlign: "center",
|
|
},
|
|
]}
|
|
>
|
|
No Activity Yet
|
|
</Text>
|
|
<Text
|
|
style={[
|
|
typography.body,
|
|
{
|
|
color: colors.textSecondary,
|
|
marginTop: 8,
|
|
textAlign: "center",
|
|
},
|
|
]}
|
|
>
|
|
Start by checking in at the gym or completing a goal!
|
|
</Text>
|
|
</MinimalCard>
|
|
) : (
|
|
<View style={styles.activityList}>
|
|
{recentActivities.map((activity) => {
|
|
const iconName = getActivityIcon(activity.type);
|
|
const emoji = getActivityEmoji(activity.type);
|
|
const timeStr = formatActivityTime(activity.timestamp);
|
|
let iconColor = colors.primary;
|
|
if (activity.type === ActivityType.GOAL_COMPLETED)
|
|
iconColor = colors.success;
|
|
else if (activity.type === ActivityType.GOAL_CREATED)
|
|
iconColor = colors.accent;
|
|
|
|
return (
|
|
<MinimalCard
|
|
key={activity.id}
|
|
variant="bordered"
|
|
style={styles.activityItem}
|
|
>
|
|
<View
|
|
style={[
|
|
styles.activityIconContainer,
|
|
{ backgroundColor: `${iconColor}15` },
|
|
]}
|
|
>
|
|
<Text style={{ fontSize: 22 }}>{emoji}</Text>
|
|
</View>
|
|
<View style={styles.activityInfo}>
|
|
<Text
|
|
style={[
|
|
typography.bodyEmphasis,
|
|
{ color: colors.textPrimary },
|
|
]}
|
|
>
|
|
{activity.title}
|
|
</Text>
|
|
<Text
|
|
style={[
|
|
typography.caption,
|
|
{ color: colors.textTertiary, marginTop: 2 },
|
|
]}
|
|
>
|
|
{timeStr}
|
|
</Text>
|
|
</View>
|
|
{activity.duration && (
|
|
<Text
|
|
style={[
|
|
typography.caption,
|
|
{ color: colors.textSecondary },
|
|
]}
|
|
>
|
|
{formatDuration(activity.duration)}
|
|
</Text>
|
|
)}
|
|
</MinimalCard>
|
|
);
|
|
})}
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
<View style={{ height: 100 }} />
|
|
</ScrollView>
|
|
|
|
<TrackMealModal
|
|
visible={trackMealModalVisible}
|
|
onClose={() => setTrackMealModalVisible(false)}
|
|
onSave={handleSaveMeal}
|
|
onResetData={handleResetCalories}
|
|
/>
|
|
<AddWaterModal
|
|
visible={addWaterModalVisible}
|
|
onClose={() => setAddWaterModalVisible(false)}
|
|
onAdd={handleAddWater}
|
|
onResetData={handleResetWater}
|
|
/>
|
|
<ScanFoodModal
|
|
visible={scanFoodModalVisible}
|
|
onClose={() => setScanFoodModalVisible(false)}
|
|
onAddFood={handleAddScannedFood}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: { flex: 1 },
|
|
scrollContent: { paddingTop: 60, paddingBottom: 20 },
|
|
section: { paddingHorizontal: 20, marginBottom: 28 },
|
|
|
|
// Hero Header
|
|
heroHeader: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
paddingHorizontal: 20,
|
|
marginBottom: 24,
|
|
},
|
|
heroTextContainer: { flex: 1, marginRight: 16 },
|
|
heroAvatar: {
|
|
width: 64,
|
|
height: 64,
|
|
borderRadius: 20,
|
|
borderWidth: 3,
|
|
borderColor: "#0066FF",
|
|
},
|
|
heroPlaceholderAvatar: {
|
|
width: 64,
|
|
height: 64,
|
|
borderRadius: 20,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
borderWidth: 3,
|
|
borderColor: "#0066FF",
|
|
},
|
|
|
|
// Activity Rings
|
|
ringsCard: { padding: 24 },
|
|
ringsContainer: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-around",
|
|
alignItems: "center",
|
|
},
|
|
streakBanner: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
marginTop: 24,
|
|
padding: 16,
|
|
borderRadius: 14,
|
|
},
|
|
|
|
// Quick Actions
|
|
quickActionsGrid: { flexDirection: "row", flexWrap: "wrap", gap: 12 },
|
|
quickActionCard: {
|
|
width: "47%",
|
|
padding: 20,
|
|
borderRadius: 20,
|
|
},
|
|
quickActionIcon: {
|
|
width: 52,
|
|
height: 52,
|
|
borderRadius: 14,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
|
|
// Progress Cards
|
|
progressCard: { padding: 20 },
|
|
progressHeader: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
},
|
|
progressLabelRow: { flexDirection: "row", alignItems: "center" },
|
|
progressIcon: {
|
|
width: 48,
|
|
height: 48,
|
|
borderRadius: 14,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
|
|
// Activity
|
|
activityList: { gap: 12 },
|
|
activityItem: { flexDirection: "row", alignItems: "center", padding: 16 },
|
|
activityIconContainer: {
|
|
width: 48,
|
|
height: 48,
|
|
borderRadius: 14,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
activityInfo: { flex: 1, marginLeft: 14 },
|
|
|
|
// Empty
|
|
emptyCard: { alignItems: "center", padding: 40 },
|
|
});
|