recent activity feed

updated, now showing real data
This commit is contained in:
echo 2026-03-12 16:25:57 +01:00
parent 981208ab7b
commit 96db3ea3b7
3 changed files with 351 additions and 56 deletions

Binary file not shown.

View File

@ -9,12 +9,13 @@ import {
TouchableOpacity, TouchableOpacity,
} from "react-native"; } from "react-native";
import { useUser } from "@clerk/clerk-expo"; import { useUser } from "@clerk/clerk-expo";
import { useState, useCallback, useEffect, useRef } from "react"; import { useState, useCallback, useEffect, useRef, useMemo } from "react";
import { useFocusEffect } from "@react-navigation/native"; import { useFocusEffect } from "@react-navigation/native";
import AsyncStorage from "@react-native-async-storage/async-storage"; import AsyncStorage from "@react-native-async-storage/async-storage";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { useTheme } from "../../contexts/ThemeContext"; import { useTheme } from "../../contexts/ThemeContext";
import { useStatistics } from "../../contexts/StatisticsContext"; import { useStatistics } from "../../contexts/StatisticsContext";
import { useFitnessGoals } from "../../contexts/FitnessGoalsContext";
import { MinimalCard } from "../../components/MinimalCard"; import { MinimalCard } from "../../components/MinimalCard";
import { SectionHeader } from "../../components/SectionHeader"; import { SectionHeader } from "../../components/SectionHeader";
import { IconContainer } from "../../components/IconContainer"; import { IconContainer } from "../../components/IconContainer";
@ -22,6 +23,18 @@ import { ProgressBar } from "../../components/ProgressBar";
import { TrackMealModal } from "../../components/TrackMealModal"; import { TrackMealModal } from "../../components/TrackMealModal";
import { AddWaterModal } from "../../components/AddWaterModal"; import { AddWaterModal } from "../../components/AddWaterModal";
import { ScanFoodModal } from "../../components/ScanFoodModal"; import { ScanFoodModal } from "../../components/ScanFoodModal";
import {
checkInsToActivities,
completedGoalsToActivities,
newGoalsToActivities,
combineActivities,
getRecentActivities,
formatActivityTime,
formatDuration,
getActivityIcon,
getActivityEmoji,
ActivityType,
} from "../../utils/activityFeed";
const CALORIE_GOAL = 2000; // kcal const CALORIE_GOAL = 2000; // kcal
const WATER_GOAL = 2000; // ml const WATER_GOAL = 2000; // ml
@ -31,6 +44,7 @@ export default function HomeScreen() {
const { colors, typography } = useTheme(); const { colors, typography } = useTheme();
const { refetchStatistics, forceRefresh, statistics, loading } = const { refetchStatistics, forceRefresh, statistics, loading } =
useStatistics(); useStatistics();
const { goals } = useFitnessGoals();
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [trackMealModalVisible, setTrackMealModalVisible] = useState(false); const [trackMealModalVisible, setTrackMealModalVisible] = useState(false);
const [addWaterModalVisible, setAddWaterModalVisible] = useState(false); const [addWaterModalVisible, setAddWaterModalVisible] = useState(false);
@ -205,6 +219,29 @@ export default function HomeScreen() {
const checkInsThisWeek = statistics?.attendance.checkInsThisWeek || 0; const checkInsThisWeek = statistics?.attendance.checkInsThisWeek || 0;
const currentStreak = statistics?.attendance.currentStreak || 0; const currentStreak = statistics?.attendance.currentStreak || 0;
// Combine activities from multiple sources
const recentActivities = useMemo(() => {
if (!statistics || !goals) return [];
const checkInActivities = checkInsToActivities(statistics.attendance).slice(
0,
2,
); // Only last 2 check-ins
const completedGoalActivities = completedGoalsToActivities(goals).slice(
0,
2,
); // Only last 2 completed goals
const newGoalActivities = newGoalsToActivities(goals, 7); // Last 7 days
const allActivities = combineActivities(
checkInActivities,
completedGoalActivities,
newGoalActivities,
);
return getRecentActivities(allActivities, 5); // Show top 5
}, [statistics, goals]);
// Check for midnight reset // Check for midnight reset
useEffect(() => { useEffect(() => {
const checkAndResetIfNeeded = async () => { const checkAndResetIfNeeded = async () => {
@ -657,52 +694,79 @@ export default function HomeScreen() {
{/* Recent Activity */} {/* Recent Activity */}
<View style={styles.section}> <View style={styles.section}>
<SectionHeader <SectionHeader
title="Recent Activity" title="📊 Recent Activity"
actionLabel="See All" subtitle={
onActionPress={() => console.log("See all tapped")} recentActivities.length === 0
? "Your activity will appear here"
: `${recentActivities.length} recent activities`
}
/> />
{recentActivities.length === 0 ? (
<MinimalCard variant="default" style={{ borderRadius: 20 }}>
<View style={styles.emptyActivity}>
<Text style={{ fontSize: 64 }}>🏃</Text>
<Text
style={[
typography.bodyEmphasis,
{ color: colors.textPrimary, marginTop: 16 },
]}
>
No Recent Activity
</Text>
<Text
style={[
typography.body,
{
color: colors.textSecondary,
marginTop: 8,
textAlign: "center",
},
]}
>
Check in at the gym or complete a goal to get started! 💪
</Text>
</View>
</MinimalCard>
) : (
<View style={styles.activityList}> <View style={styles.activityList}>
<MinimalCard variant="default" style={styles.activityItem}> {recentActivities.map((activity) => {
<IconContainer const iconName = getActivityIcon(activity.type);
variant="colored" const emoji = getActivityEmoji(activity.type);
backgroundColor={`${colors.primary}20`} const timeStr = formatActivityTime(activity.timestamp);
> const durationStr = formatDuration(activity.duration);
<Ionicons name="barbell" size={20} color={colors.primary} />
</IconContainer>
<View style={styles.activityInfo}>
<Text style={[typography.h3, { color: colors.textPrimary }]}>
Upper Body Power
</Text>
<Text
style={[
typography.caption,
{ color: colors.textTertiary, marginTop: 2 },
]}
>
Today, 10:00 AM
</Text>
</View>
<Text
style={[
typography.bodyEmphasis,
{ color: colors.textSecondary },
]}
>
45m
</Text>
</MinimalCard>
<MinimalCard variant="default" style={styles.activityItem}> // Determine color based on activity type
let iconColor = colors.primary;
if (activity.type === ActivityType.GOAL_COMPLETED) {
iconColor = colors.success;
} else if (activity.type === ActivityType.GOAL_CREATED) {
iconColor = colors.accent;
} else if (activity.type === ActivityType.GYM_CHECKIN) {
iconColor = colors.primary;
}
return (
<MinimalCard
key={activity.id}
variant="bordered"
style={styles.activityItem}
>
<IconContainer <IconContainer
variant="colored" variant="colored"
backgroundColor={`${colors.success}20`} backgroundColor={`${iconColor}20`}
> >
<Ionicons name="bicycle" size={20} color={colors.success} /> <Ionicons
name={iconName as any}
size={20}
color={iconColor}
/>
</IconContainer> </IconContainer>
<View style={styles.activityInfo}> <View style={styles.activityInfo}>
<Text style={[typography.h3, { color: colors.textPrimary }]}> <Text
Morning Cardio style={[typography.h3, { color: colors.textPrimary }]}
>
{emoji} {activity.title}
</Text> </Text>
<Text <Text
style={[ style={[
@ -710,19 +774,24 @@ export default function HomeScreen() {
{ color: colors.textTertiary, marginTop: 2 }, { color: colors.textTertiary, marginTop: 2 },
]} ]}
> >
Yesterday, 7:30 AM {timeStr}
</Text> </Text>
</View> </View>
{durationStr && (
<Text <Text
style={[ style={[
typography.bodyEmphasis, typography.bodyEmphasis,
{ color: colors.textSecondary }, { color: colors.textSecondary },
]} ]}
> >
30m {durationStr}
</Text> </Text>
)}
</MinimalCard> </MinimalCard>
);
})}
</View> </View>
)}
</View> </View>
<TrackMealModal <TrackMealModal
@ -850,8 +919,14 @@ const styles = StyleSheet.create({
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
gap: 12, gap: 12,
borderRadius: 16,
}, },
activityInfo: { activityInfo: {
flex: 1, flex: 1,
}, },
emptyActivity: {
alignItems: "center",
paddingVertical: 40,
paddingHorizontal: 20,
},
}); });

View File

@ -0,0 +1,220 @@
/**
* Activity Feed Utilities
*
* Combines multiple data sources (check-ins, goals, nutrition) into a unified activity feed
*/
import type { AttendanceStatistics } from "../api/types";
import type { FitnessGoal } from "../services/fitnessGoals";
export enum ActivityType {
GYM_CHECKIN = "gym_checkin",
GOAL_COMPLETED = "goal_completed",
GOAL_CREATED = "goal_created",
NUTRITION_MILESTONE = "nutrition_milestone",
}
export interface ActivityItem {
id: string;
type: ActivityType;
title: string;
timestamp: Date;
duration?: number; // in minutes
metadata?: Record<string, any>;
}
/**
* Convert attendance check-ins to activity items
*/
export function checkInsToActivities(
attendance: AttendanceStatistics,
): ActivityItem[] {
return attendance.recentCheckIns.map((checkIn) => {
const checkInDate = new Date(checkIn.checkInTime);
return {
id: `checkin-${checkIn.id}`,
type: ActivityType.GYM_CHECKIN,
title: "Gym Check-in",
timestamp: checkInDate,
duration: checkIn.duration || undefined,
metadata: {
checkOutTime: checkIn.checkOutTime,
},
};
});
}
/**
* Convert completed goals to activity items
*/
export function completedGoalsToActivities(
goals: FitnessGoal[],
): ActivityItem[] {
return goals
.filter((goal) => goal.status === "completed" && goal.completedDate)
.map((goal) => {
const completedDate = new Date(goal.completedDate!);
return {
id: `goal-completed-${goal.id}`,
type: ActivityType.GOAL_COMPLETED,
title: goal.title,
timestamp: completedDate,
metadata: {
goalType: goal.goalType,
priority: goal.priority,
targetValue: goal.targetValue,
unit: goal.unit,
},
};
});
}
/**
* Convert recently created goals to activity items
*/
export function newGoalsToActivities(
goals: FitnessGoal[],
daysBack: number = 7,
): ActivityItem[] {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysBack);
return goals
.filter((goal) => {
const createdDate = new Date(goal.createdAt);
return goal.status === "active" && createdDate > cutoffDate;
})
.map((goal) => {
const createdDate = new Date(goal.createdAt);
return {
id: `goal-created-${goal.id}`,
type: ActivityType.GOAL_CREATED,
title: goal.title,
timestamp: createdDate,
metadata: {
goalType: goal.goalType,
priority: goal.priority,
},
};
});
}
/**
* Combine and sort all activities by timestamp (most recent first)
*/
export function combineActivities(
...activityGroups: ActivityItem[][]
): ActivityItem[] {
const allActivities = activityGroups.flat();
return allActivities.sort(
(a, b) => b.timestamp.getTime() - a.timestamp.getTime(),
);
}
/**
* Get recent activities limited to a specific count
*/
export function getRecentActivities(
activities: ActivityItem[],
limit: number = 5,
): ActivityItem[] {
return activities.slice(0, limit);
}
/**
* Format activity timestamp for display
*/
export function formatActivityTime(timestamp: Date): string {
const now = new Date();
const diffMs = now.getTime() - timestamp.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
// Today
if (diffDays === 0) {
if (diffMins < 60) {
return diffMins <= 1 ? "Just now" : `${diffMins}m ago`;
}
return `Today, ${timestamp.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}`;
}
// Yesterday
if (diffDays === 1) {
return `Yesterday, ${timestamp.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}`;
}
// This week (within 7 days)
if (diffDays < 7) {
return `${diffDays}d ago`;
}
// Older
return timestamp.toLocaleDateString();
}
/**
* Format duration in minutes to human-readable string
*/
export function formatDuration(minutes?: number): string | undefined {
if (!minutes) return undefined;
if (minutes < 60) {
return `${minutes}m`;
}
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
if (mins === 0) {
return `${hours}h`;
}
return `${hours}h ${mins}m`;
}
/**
* Get icon name for activity type
*/
export function getActivityIcon(type: ActivityType): string {
switch (type) {
case ActivityType.GYM_CHECKIN:
return "barbell";
case ActivityType.GOAL_COMPLETED:
return "trophy";
case ActivityType.GOAL_CREATED:
return "flag";
case ActivityType.NUTRITION_MILESTONE:
return "restaurant";
default:
return "checkmark-circle";
}
}
/**
* Get activity title prefix/emoji
*/
export function getActivityEmoji(type: ActivityType): string {
switch (type) {
case ActivityType.GYM_CHECKIN:
return "💪";
case ActivityType.GOAL_COMPLETED:
return "🏆";
case ActivityType.GOAL_CREATED:
return "🎯";
case ActivityType.NUTRITION_MILESTONE:
return "🍽️";
default:
return "✅";
}
}