notification system implemented
need refinements
This commit is contained in:
1 parent
612259f020
commit
1143f8ca02
33 files changed
+2221
-222
No files matched your search
@@ -0,0 +1,434 @@
|
||||
import React, { useEffect } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
Modal,
|
||||
TouchableOpacity,
|
||||
FlatList,
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
StatusBar,
|
||||
Platform,
|
||||
} from "react-native";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { theme } from "../styles/theme";
|
||||
import { useNotifications } from "../contexts/NotificationsContext";
|
||||
import type { Notification } from "../api/notifications";
|
||||
import log from "../utils/logger";
|
||||
|
||||
interface NotificationsModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function NotificationsModal({
|
||||
visible,
|
||||
onClose,
|
||||
}: NotificationsModalProps) {
|
||||
const {
|
||||
notifications,
|
||||
loading,
|
||||
markNotificationAsRead,
|
||||
deleteNotificationAction,
|
||||
markAllAsReadAction,
|
||||
} = useNotifications();
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
log.debug("NotificationsModal opened", {
|
||||
notificationCount: notifications.length,
|
||||
loading,
|
||||
});
|
||||
}
|
||||
}, [visible, notifications.length, loading]);
|
||||
|
||||
const handleMarkAsRead = async (id: string) => {
|
||||
await markNotificationAsRead(id);
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
Alert.alert("Delete Notification", "Are you sure?", [
|
||||
{ text: "Cancel", style: "cancel" },
|
||||
{
|
||||
text: "Delete",
|
||||
style: "destructive",
|
||||
onPress: () => deleteNotificationAction(id),
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const handleMarkAllAsRead = () => {
|
||||
if (notifications.filter((n) => !n.read).length === 0) return;
|
||||
Alert.alert("Mark All as Read", "Mark all notifications as read?", [
|
||||
{ text: "Cancel", style: "cancel" },
|
||||
{
|
||||
text: "Mark All",
|
||||
onPress: () => markAllAsReadAction(),
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
animationType="slide"
|
||||
presentationStyle="fullScreen"
|
||||
onRequestClose={onClose}
|
||||
transparent={false}
|
||||
statusBarTranslucent={false}
|
||||
>
|
||||
<View style={styles.modalWrapper}>
|
||||
<StatusBar barStyle="light-content" />
|
||||
<View style={styles.container}>
|
||||
{/* Header */}
|
||||
<LinearGradient
|
||||
colors={theme.gradients.primary}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={styles.header}
|
||||
>
|
||||
<View style={styles.headerContent}>
|
||||
<View>
|
||||
<Text style={styles.headerTitle}>Notifications</Text>
|
||||
<Text style={styles.headerSubtitle}>
|
||||
{notifications.filter((n) => !n.read).length} unread
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
|
||||
<Ionicons name="close" size={28} color="#fff" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
|
||||
{/* Actions Bar */}
|
||||
{notifications.length > 0 && (
|
||||
<View style={styles.actionsBar}>
|
||||
<TouchableOpacity
|
||||
onPress={handleMarkAllAsRead}
|
||||
disabled={notifications.filter((n) => !n.read).length === 0}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.actionText,
|
||||
notifications.filter((n) => !n.read).length === 0 &&
|
||||
styles.actionTextDisabled,
|
||||
]}
|
||||
>
|
||||
Mark all as read
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Notifications List */}
|
||||
{loading ? (
|
||||
<View style={styles.centered}>
|
||||
<ActivityIndicator size="large" color={theme.colors.primary} />
|
||||
</View>
|
||||
) : notifications.length === 0 ? (
|
||||
<View style={styles.emptyState}>
|
||||
<LinearGradient
|
||||
colors={["rgba(59, 130, 246, 0.1)", "rgba(59, 130, 246, 0.05)"]}
|
||||
style={styles.emptyCard}
|
||||
>
|
||||
<Ionicons
|
||||
name="notifications-off-outline"
|
||||
size={64}
|
||||
color={theme.colors.primary}
|
||||
/>
|
||||
<Text style={styles.emptyTitle}>No Notifications</Text>
|
||||
<Text style={styles.emptyText}>
|
||||
You're all caught up! New notifications will appear here.
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={notifications}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<NotificationItem
|
||||
notification={item}
|
||||
onMarkAsRead={handleMarkAsRead}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
)}
|
||||
contentContainerStyle={styles.listContent}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
interface NotificationItemProps {
|
||||
notification: Notification;
|
||||
onMarkAsRead: (id: string) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
function NotificationItem({
|
||||
notification,
|
||||
onMarkAsRead,
|
||||
onDelete,
|
||||
}: NotificationItemProps) {
|
||||
const getIcon = (type: Notification["type"]) => {
|
||||
switch (type) {
|
||||
case "payment_reminder":
|
||||
return "card-outline";
|
||||
case "attendance":
|
||||
return "checkmark-circle-outline";
|
||||
case "promotion":
|
||||
return "megaphone-outline";
|
||||
case "system":
|
||||
default:
|
||||
return "information-circle-outline";
|
||||
}
|
||||
};
|
||||
|
||||
const getIconColor = (type: Notification["type"]) => {
|
||||
switch (type) {
|
||||
case "payment_reminder":
|
||||
return theme.colors.warning;
|
||||
case "attendance":
|
||||
return theme.colors.success;
|
||||
case "promotion":
|
||||
return theme.colors.purple;
|
||||
case "system":
|
||||
default:
|
||||
return theme.colors.primary;
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (date: Date) => {
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - new Date(date).getTime();
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (minutes < 1) return "Just now";
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
if (days < 7) return `${days}d ago`;
|
||||
return new Date(date).toLocaleDateString();
|
||||
};
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => !notification.read && onMarkAsRead(notification.id)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View
|
||||
style={[styles.notificationCard, !notification.read && styles.unread]}
|
||||
>
|
||||
<View style={styles.notificationContent}>
|
||||
{/* Icon */}
|
||||
<View
|
||||
style={[
|
||||
styles.iconCircle,
|
||||
{ backgroundColor: `${getIconColor(notification.type)}20` },
|
||||
]}
|
||||
>
|
||||
<Ionicons
|
||||
name={getIcon(notification.type)}
|
||||
size={24}
|
||||
color={getIconColor(notification.type)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Content */}
|
||||
<View style={styles.textContent}>
|
||||
<Text
|
||||
style={[
|
||||
styles.notificationTitle,
|
||||
!notification.read && styles.notificationTitleUnread,
|
||||
]}
|
||||
>
|
||||
{notification.title}
|
||||
</Text>
|
||||
<Text style={styles.notificationMessage}>
|
||||
{notification.message}
|
||||
</Text>
|
||||
<Text style={styles.notificationTime}>
|
||||
{formatTime(notification.createdAt)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Actions */}
|
||||
<TouchableOpacity
|
||||
onPress={() => onDelete(notification.id)}
|
||||
style={styles.deleteButton}
|
||||
>
|
||||
<Ionicons
|
||||
name="trash-outline"
|
||||
size={20}
|
||||
color={theme.colors.gray400}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Unread Indicator */}
|
||||
{!notification.read && <View style={styles.unreadDot} />}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
modalWrapper: {
|
||||
flex: 1,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.colors.background,
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
},
|
||||
header: {
|
||||
paddingTop: Platform.OS === "android" ? 50 : 60,
|
||||
paddingBottom: 20,
|
||||
paddingHorizontal: 20,
|
||||
borderBottomLeftRadius: theme.borderRadius.xl,
|
||||
borderBottomRightRadius: theme.borderRadius.xl,
|
||||
},
|
||||
headerContent: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: theme.typography.fontSize["3xl"],
|
||||
fontWeight: theme.typography.fontWeight.bold,
|
||||
color: theme.colors.white,
|
||||
},
|
||||
headerSubtitle: {
|
||||
fontSize: theme.typography.fontSize.base,
|
||||
color: "rgba(255, 255, 255, 0.9)",
|
||||
marginTop: 4,
|
||||
},
|
||||
closeButton: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.2)",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
actionsBar: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.colors.gray200,
|
||||
},
|
||||
actionText: {
|
||||
fontSize: theme.typography.fontSize.sm,
|
||||
fontWeight: theme.typography.fontWeight.semibold,
|
||||
color: theme.colors.primary,
|
||||
},
|
||||
actionTextDisabled: {
|
||||
color: theme.colors.gray400,
|
||||
},
|
||||
centered: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
emptyState: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 40,
|
||||
},
|
||||
emptyCard: {
|
||||
borderRadius: theme.borderRadius["2xl"],
|
||||
padding: 40,
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
},
|
||||
emptyTitle: {
|
||||
fontSize: theme.typography.fontSize.xl,
|
||||
fontWeight: theme.typography.fontWeight.bold,
|
||||
color: theme.colors.gray700,
|
||||
marginTop: 16,
|
||||
marginBottom: 8,
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: theme.typography.fontSize.base,
|
||||
color: theme.colors.gray500,
|
||||
textAlign: "center",
|
||||
lineHeight: 24,
|
||||
},
|
||||
listContent: {
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
notificationCard: {
|
||||
backgroundColor: theme.colors.white,
|
||||
borderRadius: theme.borderRadius.xl,
|
||||
padding: 16,
|
||||
marginBottom: 12,
|
||||
position: "relative",
|
||||
...theme.shadows.subtle,
|
||||
},
|
||||
unread: {
|
||||
backgroundColor: "rgba(59, 130, 246, 0.05)",
|
||||
borderLeftWidth: 3,
|
||||
borderLeftColor: theme.colors.primary,
|
||||
},
|
||||
notificationContent: {
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
},
|
||||
iconCircle: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 24,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginRight: 12,
|
||||
},
|
||||
textContent: {
|
||||
flex: 1,
|
||||
marginRight: 8,
|
||||
},
|
||||
notificationTitle: {
|
||||
fontSize: theme.typography.fontSize.base,
|
||||
fontWeight: theme.typography.fontWeight.semibold,
|
||||
color: theme.colors.gray700,
|
||||
marginBottom: 4,
|
||||
},
|
||||
notificationTitleUnread: {
|
||||
fontWeight: theme.typography.fontWeight.bold,
|
||||
color: theme.colors.gray900,
|
||||
},
|
||||
notificationMessage: {
|
||||
fontSize: theme.typography.fontSize.sm,
|
||||
color: theme.colors.gray600,
|
||||
lineHeight: 20,
|
||||
marginBottom: 6,
|
||||
},
|
||||
notificationTime: {
|
||||
fontSize: theme.typography.fontSize.xs,
|
||||
color: theme.colors.gray400,
|
||||
},
|
||||
deleteButton: {
|
||||
padding: 4,
|
||||
},
|
||||
unreadDot: {
|
||||
position: "absolute",
|
||||
top: 16,
|
||||
right: 16,
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.colors.primary,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
import React from "react";
|
||||
import { View, Text, StyleSheet } from "react-native";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { theme } from "../styles/theme";
|
||||
|
||||
interface NutritionWidgetProps {
|
||||
current: number; // in kcal
|
||||
goal: number; // in kcal
|
||||
}
|
||||
|
||||
export function NutritionWidget({ current, goal }: NutritionWidgetProps) {
|
||||
const percentage = Math.min(Math.max(current / goal, 0), 1);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<LinearGradient
|
||||
colors={["#fef3c7", "#fde68a"]} // Light yellow background
|
||||
style={[styles.card, theme.shadows.subtle]}
|
||||
>
|
||||
<View style={styles.content}>
|
||||
<View style={styles.iconContainer}>
|
||||
<LinearGradient colors={theme.gradients.sunset} style={styles.icon}>
|
||||
<Ionicons name="nutrition" size={24} color="#fff" />
|
||||
</LinearGradient>
|
||||
</View>
|
||||
|
||||
<View style={styles.info}>
|
||||
<Text style={styles.title}>Nutrition</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
{current} / {goal} kcal
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.percentageContainer}>
|
||||
<Text style={styles.percentage}>
|
||||
{Math.round(percentage * 100)}%
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.progressContainer}>
|
||||
<View style={styles.progressBarBg}>
|
||||
<LinearGradient
|
||||
colors={theme.gradients.sunset}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
style={[
|
||||
styles.progressBarFill,
|
||||
{ width: `${percentage * 100}%` },
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginHorizontal: 20,
|
||||
marginBottom: 24,
|
||||
},
|
||||
card: {
|
||||
borderRadius: 24,
|
||||
padding: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: "#fcd34d",
|
||||
},
|
||||
content: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginBottom: 16,
|
||||
},
|
||||
iconContainer: {
|
||||
marginRight: 16,
|
||||
},
|
||||
icon: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 16,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
info: {
|
||||
flex: 1,
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
fontWeight: "700",
|
||||
color: theme.colors.gray900,
|
||||
marginBottom: 4,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 14,
|
||||
color: theme.colors.gray600,
|
||||
fontWeight: "500",
|
||||
},
|
||||
percentageContainer: {
|
||||
backgroundColor: "#fff",
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 12,
|
||||
},
|
||||
percentage: {
|
||||
fontSize: 14,
|
||||
fontWeight: "700",
|
||||
color: theme.colors.primary,
|
||||
},
|
||||
progressContainer: {
|
||||
height: 8,
|
||||
},
|
||||
progressBarBg: {
|
||||
height: 8,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.5)",
|
||||
borderRadius: 4,
|
||||
overflow: "hidden",
|
||||
},
|
||||
progressBarFill: {
|
||||
height: "100%",
|
||||
borderRadius: 4,
|
||||
},
|
||||
});
|
||||
@@ -1,123 +1,145 @@
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, Dimensions } from 'react-native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { theme } from '../styles/theme';
|
||||
import React from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
Dimensions,
|
||||
} from "react-native";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { theme } from "../styles/theme";
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
const { width } = Dimensions.get("window");
|
||||
const ITEM_WIDTH = (width - 40 - 16) / 2; // (Screen width - padding - gap) / 2
|
||||
|
||||
interface QuickActionProps {
|
||||
icon: keyof typeof Ionicons.glyphMap;
|
||||
label: string;
|
||||
gradient: readonly [string, string, ...string[]];
|
||||
onPress?: () => void;
|
||||
icon: keyof typeof Ionicons.glyphMap;
|
||||
label: string;
|
||||
gradient: readonly [string, string, ...string[]];
|
||||
onPress?: () => void;
|
||||
}
|
||||
|
||||
function QuickActionItem({ icon, label, gradient, onPress }: QuickActionProps) {
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress} activeOpacity={0.8} style={styles.itemContainer}>
|
||||
<LinearGradient
|
||||
colors={['rgba(255, 255, 255, 0.8)', 'rgba(255, 255, 255, 0.6)']}
|
||||
style={[styles.item, theme.shadows.subtle]}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={gradient}
|
||||
style={styles.iconContainer}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
>
|
||||
<Ionicons name={icon} size={24} color="#fff" />
|
||||
</LinearGradient>
|
||||
<Text style={styles.label}>{label}</Text>
|
||||
<Ionicons name="chevron-forward" size={16} color={theme.colors.gray400} style={styles.arrow} />
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
activeOpacity={0.8}
|
||||
style={styles.itemContainer}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={["rgba(255, 255, 255, 0.8)", "rgba(255, 255, 255, 0.6)"]}
|
||||
style={[styles.item, theme.shadows.subtle]}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={gradient}
|
||||
style={styles.iconContainer}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
>
|
||||
<Ionicons name={icon} size={24} color="#fff" />
|
||||
</LinearGradient>
|
||||
<Text style={styles.label}>{label}</Text>
|
||||
<Ionicons
|
||||
name="chevron-forward"
|
||||
size={16}
|
||||
color={theme.colors.gray400}
|
||||
style={styles.arrow}
|
||||
/>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
interface QuickActionGridProps {
|
||||
onTrackMealPress?: () => void;
|
||||
onAddWaterPress?: () => void;
|
||||
onScanFoodPress?: () => void;
|
||||
onTrackMealPress?: () => void;
|
||||
onAddWaterPress?: () => void;
|
||||
onScanFoodPress?: () => void;
|
||||
onLogWorkoutPress?: () => void;
|
||||
}
|
||||
|
||||
export function QuickActionGrid({ onTrackMealPress, onAddWaterPress, onScanFoodPress }: QuickActionGridProps) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.sectionTitle}>Quick Actions</Text>
|
||||
<View style={styles.grid}>
|
||||
<QuickActionItem
|
||||
icon="barbell"
|
||||
label="Log Workout"
|
||||
gradient={theme.gradients.primary}
|
||||
/>
|
||||
<QuickActionItem
|
||||
icon="restaurant"
|
||||
label="Track Meal"
|
||||
gradient={theme.gradients.success}
|
||||
onPress={onTrackMealPress}
|
||||
/>
|
||||
<QuickActionItem
|
||||
icon="water"
|
||||
label="Add Water"
|
||||
gradient={theme.gradients.ocean}
|
||||
onPress={onAddWaterPress}
|
||||
/>
|
||||
<QuickActionItem
|
||||
icon="scan"
|
||||
label="Scan Food"
|
||||
gradient={theme.gradients.purple}
|
||||
onPress={onScanFoodPress}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
export function QuickActionGrid({
|
||||
onTrackMealPress,
|
||||
onAddWaterPress,
|
||||
onScanFoodPress,
|
||||
onLogWorkoutPress,
|
||||
}: QuickActionGridProps) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.sectionTitle}>Quick Actions</Text>
|
||||
<View style={styles.grid}>
|
||||
<QuickActionItem
|
||||
icon="barbell"
|
||||
label="Log Workout"
|
||||
gradient={theme.gradients.primary}
|
||||
onPress={onLogWorkoutPress}
|
||||
/>
|
||||
<QuickActionItem
|
||||
icon="restaurant"
|
||||
label="Track Meal"
|
||||
gradient={theme.gradients.success}
|
||||
onPress={onTrackMealPress}
|
||||
/>
|
||||
<QuickActionItem
|
||||
icon="water"
|
||||
label="Add Water"
|
||||
gradient={theme.gradients.ocean}
|
||||
onPress={onAddWaterPress}
|
||||
/>
|
||||
<QuickActionItem
|
||||
icon="scan"
|
||||
label="Scan Food"
|
||||
gradient={theme.gradients.purple}
|
||||
onPress={onScanFoodPress}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingHorizontal: 20,
|
||||
marginBottom: 24,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: theme.colors.gray900,
|
||||
marginBottom: 16,
|
||||
},
|
||||
grid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 16,
|
||||
},
|
||||
itemContainer: {
|
||||
width: ITEM_WIDTH,
|
||||
},
|
||||
item: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#fff',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255, 255, 255, 0.6)',
|
||||
},
|
||||
iconContainer: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 14,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 12,
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: theme.colors.gray800,
|
||||
flex: 1,
|
||||
},
|
||||
arrow: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
container: {
|
||||
paddingHorizontal: 20,
|
||||
marginBottom: 24,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: "700",
|
||||
color: theme.colors.gray900,
|
||||
marginBottom: 16,
|
||||
},
|
||||
grid: {
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
gap: 16,
|
||||
},
|
||||
itemContainer: {
|
||||
width: ITEM_WIDTH,
|
||||
},
|
||||
item: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
padding: 16,
|
||||
borderRadius: 20,
|
||||
backgroundColor: "#fff",
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255, 255, 255, 0.6)",
|
||||
},
|
||||
iconContainer: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 14,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginRight: 12,
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
color: theme.colors.gray800,
|
||||
flex: 1,
|
||||
},
|
||||
arrow: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import { Ionicons } from "@expo/vector-icons";
|
||||
import { theme } from "../styles/theme";
|
||||
import { useStatistics } from "../contexts/StatisticsContext";
|
||||
import type { WeeklyTrendData } from "../api/types";
|
||||
import log from "../utils/logger";
|
||||
|
||||
export function WeeklyProgressWidget() {
|
||||
const { statistics, loading, refetchStatistics } = useStatistics();
|
||||
@@ -16,9 +17,24 @@ export function WeeklyProgressWidget() {
|
||||
|
||||
useEffect(() => {
|
||||
if (statistics?.weeklyTrend) {
|
||||
log.debug("WeeklyProgressWidget - Processing weekly trend", {
|
||||
weeklyTrendLength: statistics.weeklyTrend.length,
|
||||
weeklyTrend: statistics.weeklyTrend,
|
||||
statisticsKeys: Object.keys(statistics),
|
||||
});
|
||||
|
||||
// Get last 4 weeks for compact display
|
||||
const last4Weeks = statistics.weeklyTrend.slice(-4);
|
||||
setWeeklyData(last4Weeks);
|
||||
|
||||
log.debug("WeeklyProgressWidget - Set weekly data", {
|
||||
last4Weeks,
|
||||
});
|
||||
} else {
|
||||
log.debug("WeeklyProgressWidget - No weekly trend data", {
|
||||
hasStatistics: !!statistics,
|
||||
statistics,
|
||||
});
|
||||
}
|
||||
}, [statistics]);
|
||||
|
||||
|
||||
Reference in new issue
Block a user