recent activity feed
updated, now showing real data
This commit is contained in:
parent
981208ab7b
commit
96db3ea3b7
Binary file not shown.
@ -9,12 +9,13 @@ import {
|
||||
TouchableOpacity,
|
||||
} from "react-native";
|
||||
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 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";
|
||||
@ -22,6 +23,18 @@ import { ProgressBar } from "../../components/ProgressBar";
|
||||
import { TrackMealModal } from "../../components/TrackMealModal";
|
||||
import { AddWaterModal } from "../../components/AddWaterModal";
|
||||
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 WATER_GOAL = 2000; // ml
|
||||
@ -31,6 +44,7 @@ export default function HomeScreen() {
|
||||
const { colors, typography } = useTheme();
|
||||
const { refetchStatistics, forceRefresh, statistics, loading } =
|
||||
useStatistics();
|
||||
const { goals } = useFitnessGoals();
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [trackMealModalVisible, setTrackMealModalVisible] = useState(false);
|
||||
const [addWaterModalVisible, setAddWaterModalVisible] = useState(false);
|
||||
@ -205,6 +219,29 @@ export default function HomeScreen() {
|
||||
const checkInsThisWeek = statistics?.attendance.checkInsThisWeek || 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
|
||||
useEffect(() => {
|
||||
const checkAndResetIfNeeded = async () => {
|
||||
@ -657,52 +694,79 @@ export default function HomeScreen() {
|
||||
{/* Recent Activity */}
|
||||
<View style={styles.section}>
|
||||
<SectionHeader
|
||||
title="Recent Activity"
|
||||
actionLabel="See All"
|
||||
onActionPress={() => console.log("See all tapped")}
|
||||
title="📊 Recent Activity"
|
||||
subtitle={
|
||||
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}>
|
||||
<MinimalCard variant="default" style={styles.activityItem}>
|
||||
<IconContainer
|
||||
variant="colored"
|
||||
backgroundColor={`${colors.primary}20`}
|
||||
>
|
||||
<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>
|
||||
{recentActivities.map((activity) => {
|
||||
const iconName = getActivityIcon(activity.type);
|
||||
const emoji = getActivityEmoji(activity.type);
|
||||
const timeStr = formatActivityTime(activity.timestamp);
|
||||
const durationStr = formatDuration(activity.duration);
|
||||
|
||||
<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
|
||||
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>
|
||||
<View style={styles.activityInfo}>
|
||||
<Text style={[typography.h3, { color: colors.textPrimary }]}>
|
||||
Morning Cardio
|
||||
<Text
|
||||
style={[typography.h3, { color: colors.textPrimary }]}
|
||||
>
|
||||
{emoji} {activity.title}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
@ -710,19 +774,24 @@ export default function HomeScreen() {
|
||||
{ color: colors.textTertiary, marginTop: 2 },
|
||||
]}
|
||||
>
|
||||
Yesterday, 7:30 AM
|
||||
{timeStr}
|
||||
</Text>
|
||||
</View>
|
||||
{durationStr && (
|
||||
<Text
|
||||
style={[
|
||||
typography.bodyEmphasis,
|
||||
{ color: colors.textSecondary },
|
||||
]}
|
||||
>
|
||||
30m
|
||||
{durationStr}
|
||||
</Text>
|
||||
)}
|
||||
</MinimalCard>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<TrackMealModal
|
||||
@ -850,8 +919,14 @@ const styles = StyleSheet.create({
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
borderRadius: 16,
|
||||
},
|
||||
activityInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
emptyActivity: {
|
||||
alignItems: "center",
|
||||
paddingVertical: 40,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
});
|
||||
|
||||
220
apps/mobile/src/utils/activityFeed.ts
Normal file
220
apps/mobile/src/utils/activityFeed.ts
Normal 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 "✅";
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user