121 lines
4.1 KiB
TypeScript
121 lines
4.1 KiB
TypeScript
import { useCallback, useState } from "react";
|
|
import { View, Text, Pressable, FlatList, Alert } from "react-native";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useNavigation } from "@react-navigation/native";
|
|
import { useFocusEffect } from "@react-navigation/native";
|
|
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
|
import type { HistoryStackParamList } from "@navigation/types";
|
|
import { getAllSessions, deleteSessionAndSync } from "@db/operations";
|
|
import { formatTime } from "@utils/formatTime";
|
|
import { formatDistance } from "@utils/formatDistance";
|
|
import { SwipeableRow } from "@components/SwipeableRow";
|
|
|
|
type SessionRow = {
|
|
id: string;
|
|
name: string | null;
|
|
totalDistance: number | null;
|
|
totalDuration: number | null;
|
|
calories: number | null;
|
|
startedAt: string;
|
|
finishedAt: string | null;
|
|
};
|
|
|
|
export function HistoryScreen() {
|
|
const { t } = useTranslation("history");
|
|
const { t: tc } = useTranslation("common");
|
|
const navigation = useNavigation<NativeStackNavigationProp<HistoryStackParamList>>();
|
|
const [sessions, setSessions] = useState<SessionRow[]>([]);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
|
|
const loadSessions = useCallback(async () => {
|
|
const data = await getAllSessions();
|
|
setSessions(data as SessionRow[]);
|
|
}, []);
|
|
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
loadSessions();
|
|
}, [loadSessions])
|
|
);
|
|
|
|
const onRefresh = async () => {
|
|
setRefreshing(true);
|
|
await loadSessions();
|
|
setRefreshing(false);
|
|
};
|
|
|
|
const onDelete = (id: string) => {
|
|
Alert.alert(tc("alert.deleteWorkout"), tc("alert.areYouSure"), [
|
|
{ text: tc("cancel"), style: "cancel" },
|
|
{
|
|
text: tc("delete"),
|
|
style: "destructive",
|
|
onPress: async () => {
|
|
await deleteSessionAndSync(id);
|
|
setSessions((prev) => prev.filter((s) => s.id !== id));
|
|
},
|
|
},
|
|
]);
|
|
};
|
|
|
|
const grouped = sessions.reduce<Record<string, SessionRow[]>>((acc, s) => {
|
|
const date = new Date(s.startedAt).toLocaleDateString();
|
|
if (!acc[date]) acc[date] = [];
|
|
acc[date].push(s);
|
|
return acc;
|
|
}, {});
|
|
|
|
const sections = Object.entries(grouped);
|
|
|
|
return (
|
|
<View className="flex-1 bg-white">
|
|
<FlatList
|
|
data={sections}
|
|
keyExtractor={([date]) => date}
|
|
contentContainerClassName="px-4 pb-8"
|
|
refreshing={refreshing}
|
|
onRefresh={onRefresh}
|
|
ListEmptyComponent={
|
|
<View className="py-12 items-center">
|
|
<Text className="text-slate-400 text-lg">{t("noCompletedYet")}</Text>
|
|
</View>
|
|
}
|
|
renderItem={({ item: [date, rows] }) => (
|
|
<View className="mb-4">
|
|
<Text className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 px-1">
|
|
{date}
|
|
</Text>
|
|
{rows.map((s) => (
|
|
<SwipeableRow key={s.id} onDelete={() => onDelete(s.id)} deleteLabel={tc("delete")}>
|
|
<Pressable
|
|
onPress={() => navigation.navigate("SessionDetail", { sessionId: s.id })}
|
|
className="p-4"
|
|
>
|
|
<View className="flex-row justify-between items-center">
|
|
<View>
|
|
<Text className="font-semibold text-slate-900">
|
|
{s.name || tc("swimSession")}
|
|
</Text>
|
|
<Text className="text-slate-500 text-sm">
|
|
{formatDistance(s.totalDistance ?? 0)}
|
|
{s.totalDuration ? ` · ${formatTime(s.totalDuration)}` : ""}
|
|
{s.calories ? ` · ${tc("kcal", { count: Math.round(s.calories) })}` : ""}
|
|
</Text>
|
|
</View>
|
|
<Text className="text-slate-400 text-xs">
|
|
{new Date(s.startedAt).toLocaleTimeString([], {
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
})}
|
|
</Text>
|
|
</View>
|
|
</Pressable>
|
|
</SwipeableRow>
|
|
))}
|
|
</View>
|
|
)}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|