feat(workouts): add swipe-to-delete with gesture handler and pull-to-refresh
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { useRef } from "react";
|
||||
import { View, Text, Pressable, Animated, StyleSheet } from "react-native";
|
||||
import { PanGestureHandler, State } from "react-native-gesture-handler";
|
||||
|
||||
type SwipeableRowProps = {
|
||||
children: React.ReactNode;
|
||||
onDelete: () => void;
|
||||
};
|
||||
|
||||
export function SwipeableRow({ children, onDelete }: SwipeableRowProps) {
|
||||
const translateX = useRef(new Animated.Value(0)).current;
|
||||
const lastOffset = useRef(0);
|
||||
|
||||
const onGestureEvent = Animated.event([{ nativeEvent: { translationX: translateX } }], {
|
||||
useNativeDriver: true,
|
||||
});
|
||||
|
||||
const onHandlerStateChange = (event: any) => {
|
||||
if (event.oldState === State.ACTIVE) {
|
||||
const { translationX } = event.nativeEvent;
|
||||
const newOffset = lastOffset.current + translationX;
|
||||
|
||||
if (newOffset < -80) {
|
||||
// Swiped left enough — show delete
|
||||
Animated.spring(translateX, {
|
||||
toValue: -80,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
lastOffset.current = -80;
|
||||
} else {
|
||||
// Snap back
|
||||
Animated.spring(translateX, {
|
||||
toValue: 0,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
lastOffset.current = 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
Animated.timing(translateX, {
|
||||
toValue: -400,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}).start(() => {
|
||||
onDelete();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.deleteBackground}>
|
||||
<Pressable onPress={handleDelete} style={styles.deleteButton}>
|
||||
<Text style={styles.deleteText}>Delete</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
<PanGestureHandler
|
||||
onGestureEvent={onGestureEvent}
|
||||
onHandlerStateChange={onHandlerStateChange}
|
||||
>
|
||||
<Animated.View style={[styles.foreground, { transform: [{ translateX }] }]}>
|
||||
{children}
|
||||
</Animated.View>
|
||||
</PanGestureHandler>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
deleteBackground: {
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: 80,
|
||||
borderRadius: 16,
|
||||
backgroundColor: "#ef4444",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
deleteButton: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
},
|
||||
deleteText: {
|
||||
color: "white",
|
||||
fontWeight: "600",
|
||||
fontSize: 13,
|
||||
},
|
||||
foreground: {
|
||||
backgroundColor: "white",
|
||||
borderRadius: 16,
|
||||
},
|
||||
});
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { View, Text, Pressable, FlatList } from "react-native";
|
||||
import { useCallback, useState } from "react";
|
||||
import { View, Text, Pressable, FlatList, Alert } from "react-native";
|
||||
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 { getAllWorkouts } from "@db/operations";
|
||||
import { getAllWorkouts, deleteWorkout } from "@db/operations";
|
||||
import { formatTime } from "@utils/formatTime";
|
||||
import { formatDistance } from "@utils/formatDistance";
|
||||
import { SwipeableRow } from "@components/SwipeableRow";
|
||||
|
||||
type WorkoutRow = {
|
||||
id: string;
|
||||
@@ -20,16 +22,39 @@ type WorkoutRow = {
|
||||
export function HistoryScreen() {
|
||||
const navigation = useNavigation<NativeStackNavigationProp<HistoryStackParamList>>();
|
||||
const [workouts, setWorkouts] = useState<WorkoutRow[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
loadWorkouts();
|
||||
}, []);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const loadWorkouts = async () => {
|
||||
const data = await getAllWorkouts();
|
||||
setWorkouts((data as WorkoutRow[]).filter((w) => w.finishedAt));
|
||||
};
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
loadWorkouts();
|
||||
}, [])
|
||||
);
|
||||
|
||||
const onRefresh = async () => {
|
||||
setRefreshing(true);
|
||||
await loadWorkouts();
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
const onDelete = (id: string) => {
|
||||
Alert.alert("Delete Workout", "Are you sure?", [
|
||||
{ text: "Cancel", style: "cancel" },
|
||||
{
|
||||
text: "Delete",
|
||||
style: "destructive",
|
||||
onPress: async () => {
|
||||
await deleteWorkout(id);
|
||||
await loadWorkouts();
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const grouped = workouts.reduce<Record<string, WorkoutRow[]>>((acc, w) => {
|
||||
const date = new Date(w.startedAt).toLocaleDateString();
|
||||
if (!acc[date]) acc[date] = [];
|
||||
@@ -41,19 +66,12 @@ export function HistoryScreen() {
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-white">
|
||||
<View className="p-4 pb-2">
|
||||
<Pressable
|
||||
onPress={() => navigation.navigate("Stats")}
|
||||
className="bg-slate-100 rounded-2xl p-3 items-center mb-4"
|
||||
>
|
||||
<Text className="text-sky-600 font-semibold">View Statistics</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<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">No completed workouts yet</Text>
|
||||
@@ -65,30 +83,31 @@ export function HistoryScreen() {
|
||||
{date}
|
||||
</Text>
|
||||
{rows.map((w) => (
|
||||
<Pressable
|
||||
key={w.id}
|
||||
onPress={() => navigation.navigate("SessionDetail", { sessionId: w.id })}
|
||||
className="bg-slate-50 rounded-2xl p-4 mb-2 border border-slate-200"
|
||||
>
|
||||
<View className="flex-row justify-between items-center">
|
||||
<View>
|
||||
<Text className="font-semibold text-slate-900">
|
||||
{w.name || "Swim Session"}
|
||||
</Text>
|
||||
<Text className="text-slate-500 text-sm">
|
||||
{formatDistance(w.totalDistance ?? 0)}
|
||||
{w.totalDuration ? ` · ${formatTime(w.totalDuration)}` : ""}
|
||||
{w.calories ? ` · ${Math.round(w.calories)} kcal` : ""}
|
||||
<SwipeableRow key={w.id} onDelete={() => onDelete(w.id)}>
|
||||
<Pressable
|
||||
onPress={() => navigation.navigate("SessionDetail", { sessionId: w.id })}
|
||||
className="p-4"
|
||||
>
|
||||
<View className="flex-row justify-between items-center">
|
||||
<View>
|
||||
<Text className="font-semibold text-slate-900">
|
||||
{w.name || "Swim Session"}
|
||||
</Text>
|
||||
<Text className="text-slate-500 text-sm">
|
||||
{formatDistance(w.totalDistance ?? 0)}
|
||||
{w.totalDuration ? ` · ${formatTime(w.totalDuration)}` : ""}
|
||||
{w.calories ? ` · ${Math.round(w.calories)} kcal` : ""}
|
||||
</Text>
|
||||
</View>
|
||||
<Text className="text-slate-400 text-xs">
|
||||
{new Date(w.startedAt).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
<Text className="text-slate-400 text-xs">
|
||||
{new Date(w.startedAt).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
</SwipeableRow>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { View, Text, ScrollView } from "react-native";
|
||||
import { View, Text, ScrollView, Pressable, Alert } from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import type { HistoryStackProps } from "@navigation/types";
|
||||
import { getWorkoutWithSets } from "@db/operations";
|
||||
import { getWorkoutWithSets, deleteWorkout } from "@db/operations";
|
||||
import { formatTime } from "@utils/formatTime";
|
||||
import { formatDistance } from "@utils/formatDistance";
|
||||
import { STROKE_COLORS } from "@constants/strokes";
|
||||
@@ -34,6 +35,7 @@ type WorkoutDetail = {
|
||||
};
|
||||
|
||||
export function SessionDetailScreen({ route }: HistoryStackProps<"SessionDetail">) {
|
||||
const navigation = useNavigation();
|
||||
const [workout, setWorkout] = useState<WorkoutDetail | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -45,6 +47,20 @@ export function SessionDetailScreen({ route }: HistoryStackProps<"SessionDetail"
|
||||
setWorkout(data as WorkoutDetail | null);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
Alert.alert("Delete Workout", "Are you sure?", [
|
||||
{ text: "Cancel", style: "cancel" },
|
||||
{
|
||||
text: "Delete",
|
||||
style: "destructive",
|
||||
onPress: async () => {
|
||||
await deleteWorkout(route.params.sessionId);
|
||||
navigation.goBack();
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
if (!workout) {
|
||||
return (
|
||||
<View className="flex-1 bg-white items-center justify-center">
|
||||
@@ -62,13 +78,21 @@ export function SessionDetailScreen({ route }: HistoryStackProps<"SessionDetail"
|
||||
|
||||
return (
|
||||
<ScrollView className="flex-1 bg-white p-4">
|
||||
<View className="items-center mb-6">
|
||||
<Text className="text-2xl font-bold text-slate-900 mb-1">
|
||||
{workout.name || "Swim Session"}
|
||||
</Text>
|
||||
<Text className="text-slate-500">
|
||||
{new Date(workout.startedAt).toLocaleDateString()}
|
||||
</Text>
|
||||
<View className="flex-row justify-between items-start mb-6">
|
||||
<View className="flex-1">
|
||||
<Text className="text-2xl font-bold text-slate-900 mb-1">
|
||||
{workout.name || "Swim Session"}
|
||||
</Text>
|
||||
<Text className="text-slate-500">
|
||||
{new Date(workout.startedAt).toLocaleDateString()}
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable
|
||||
onPress={handleDelete}
|
||||
className="bg-red-50 rounded-xl p-2.5"
|
||||
>
|
||||
<Text className="text-red-500 text-lg">🗑</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<View className="flex-row gap-3 mb-6">
|
||||
|
||||
@@ -11,7 +11,8 @@ const STROKES: StrokeType[] = ["Freestyle", "Backstroke", "Breaststroke", "Butte
|
||||
|
||||
export function BuilderScreen() {
|
||||
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||
const { poolLength } = useSettings();
|
||||
const { poolLength: defaultPoolLength } = useSettings();
|
||||
const [poolLength, setPoolLength] = useState(defaultPoolLength);
|
||||
const {
|
||||
sets,
|
||||
workoutName,
|
||||
@@ -53,6 +54,29 @@ export function BuilderScreen() {
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="mb-4">
|
||||
<Text className="text-sm font-medium text-slate-500 mb-1">Pool Length</Text>
|
||||
<View className="flex-row gap-2">
|
||||
{[25, 50].map((length) => (
|
||||
<Pressable
|
||||
key={length}
|
||||
onPress={() => setPoolLength(length)}
|
||||
className={`flex-1 py-2.5 rounded-xl items-center ${
|
||||
poolLength === length ? "bg-sky-500" : "bg-slate-100"
|
||||
}`}
|
||||
>
|
||||
<Text
|
||||
className={`font-semibold ${
|
||||
poolLength === length ? "text-white" : "text-slate-600"
|
||||
}`}
|
||||
>
|
||||
{length}m
|
||||
</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{sets.map((set, index) => (
|
||||
<View key={index} className="bg-slate-50 rounded-2xl p-4 mb-3 border border-slate-200">
|
||||
<View className="flex-row justify-between items-center mb-3">
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { View, Text, Pressable, FlatList } from "react-native";
|
||||
import { useCallback, useState } from "react";
|
||||
import { View, Text, Pressable, FlatList, Alert } from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import type { WorkoutStackParamList } from "@navigation/types";
|
||||
import { getAllWorkouts, deleteWorkout } from "@db/operations";
|
||||
import { formatTime } from "@utils/formatTime";
|
||||
import { formatDistance } from "@utils/formatDistance";
|
||||
import { SwipeableRow } from "@components/SwipeableRow";
|
||||
|
||||
type WorkoutRow = {
|
||||
id: string;
|
||||
@@ -19,37 +21,56 @@ type WorkoutRow = {
|
||||
export function WorkoutListScreen() {
|
||||
const navigation = useNavigation<NativeStackNavigationProp<WorkoutStackParamList>>();
|
||||
const [workouts, setWorkouts] = useState<WorkoutRow[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
loadWorkouts();
|
||||
}, []);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const loadWorkouts = async () => {
|
||||
const data = await getAllWorkouts();
|
||||
setWorkouts(data as WorkoutRow[]);
|
||||
};
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
loadWorkouts();
|
||||
}, [])
|
||||
);
|
||||
|
||||
const onRefresh = async () => {
|
||||
setRefreshing(true);
|
||||
await loadWorkouts();
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
const onDelete = (id: string) => {
|
||||
Alert.alert("Delete Workout", "Are you sure?", [
|
||||
{ text: "Cancel", style: "cancel" },
|
||||
{
|
||||
text: "Delete",
|
||||
style: "destructive",
|
||||
onPress: async () => {
|
||||
await deleteWorkout(id);
|
||||
await loadWorkouts();
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-white">
|
||||
<View className="p-4 pb-2">
|
||||
<Pressable
|
||||
onPress={() => navigation.navigate("Builder")}
|
||||
className="bg-sky-500 rounded-2xl p-4 items-center mb-3"
|
||||
className="bg-sky-500 rounded-2xl p-4 items-center mb-4"
|
||||
>
|
||||
<Text className="text-white font-bold text-lg">+ New Workout</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={() => navigation.navigate("Templates")}
|
||||
className="bg-slate-100 rounded-2xl p-3 items-center mb-4"
|
||||
>
|
||||
<Text className="text-sky-600 font-semibold">Browse Templates</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<FlatList
|
||||
data={workouts}
|
||||
keyExtractor={(item) => item.id}
|
||||
contentContainerClassName="px-4 pb-8"
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
ListEmptyComponent={
|
||||
<View className="py-12 items-center">
|
||||
<Text className="text-slate-400 text-lg">No workouts yet</Text>
|
||||
@@ -57,30 +78,32 @@ export function WorkoutListScreen() {
|
||||
</View>
|
||||
}
|
||||
renderItem={({ item }) => (
|
||||
<Pressable
|
||||
onPress={() => navigation.navigate("PreSwim", { workoutId: item.id })}
|
||||
className="bg-slate-50 rounded-2xl p-4 mb-3 border border-slate-200"
|
||||
>
|
||||
<View className="flex-row justify-between items-start">
|
||||
<View className="flex-1">
|
||||
<Text className="font-semibold text-slate-900 text-base">
|
||||
{item.name || "Untitled Workout"}
|
||||
</Text>
|
||||
<Text className="text-slate-500 text-sm mt-1">
|
||||
{formatDistance(item.totalDistance ?? 0)}
|
||||
{item.totalDuration ? ` · ${formatTime(item.totalDuration)}` : ""}
|
||||
</Text>
|
||||
<Text className="text-slate-400 text-xs mt-1">
|
||||
{new Date(item.startedAt).toLocaleDateString()}
|
||||
</Text>
|
||||
</View>
|
||||
{item.finishedAt && (
|
||||
<View className="bg-green-100 rounded-full px-2 py-0.5">
|
||||
<Text className="text-green-700 text-xs font-medium">Done</Text>
|
||||
<SwipeableRow onDelete={() => onDelete(item.id)}>
|
||||
<Pressable
|
||||
onPress={() => navigation.navigate("PreSwim", { workoutId: item.id })}
|
||||
className="p-4"
|
||||
>
|
||||
<View className="flex-row justify-between items-start">
|
||||
<View className="flex-1">
|
||||
<Text className="font-semibold text-slate-900 text-base">
|
||||
{item.name || "Untitled Workout"}
|
||||
</Text>
|
||||
<Text className="text-slate-500 text-sm mt-1">
|
||||
{formatDistance(item.totalDistance ?? 0)}
|
||||
{item.totalDuration ? ` · ${formatTime(item.totalDuration)}` : ""}
|
||||
</Text>
|
||||
<Text className="text-slate-400 text-xs mt-1">
|
||||
{new Date(item.startedAt).toLocaleDateString()}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Pressable>
|
||||
{item.finishedAt && (
|
||||
<View className="bg-green-100 rounded-full px-2 py-0.5">
|
||||
<Text className="text-green-700 text-xs font-medium">Done</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Pressable>
|
||||
</SwipeableRow>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||
import { HistoryStackParamList } from "./types";
|
||||
import { HistoryScreen } from "../features/history/screens/HistoryScreen";
|
||||
import { SessionDetailScreen } from "../features/history/screens/SessionDetailScreen";
|
||||
import { StatsScreen } from "../features/history/screens/StatsScreen";
|
||||
|
||||
const Stack = createNativeStackNavigator<HistoryStackParamList>();
|
||||
|
||||
@@ -25,11 +24,6 @@ export function HistoryNavigator() {
|
||||
component={SessionDetailScreen}
|
||||
options={{ title: "Session Detail" }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Stats"
|
||||
component={StatsScreen}
|
||||
options={{ title: "Statistics" }}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { BuilderScreen } from "../features/workout-builder/screens/BuilderScreen
|
||||
import { PreSwimScreen } from "../features/active-workout/screens/PreSwimScreen";
|
||||
import { PostSwimScreen } from "../features/active-workout/screens/PostSwimScreen";
|
||||
import { SessionDetailScreen } from "../features/history/screens/SessionDetailScreen";
|
||||
import { StatsScreen } from "../features/history/screens/StatsScreen";
|
||||
|
||||
const Stack = createNativeStackNavigator<RootStackParamList>();
|
||||
|
||||
@@ -43,11 +42,6 @@ export function RootNavigator() {
|
||||
component={SessionDetailScreen}
|
||||
options={{ title: "Session" }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Stats"
|
||||
component={StatsScreen}
|
||||
options={{ title: "Statistics" }}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||
import { WorkoutStackParamList } from "./types";
|
||||
import { WorkoutListScreen } from "../features/workout-builder/screens/WorkoutListScreen";
|
||||
import { BuilderScreen } from "../features/workout-builder/screens/BuilderScreen";
|
||||
import { TemplatesScreen } from "../features/templates/screens/TemplatesScreen";
|
||||
import { PreSwimScreen } from "../features/active-workout/screens/PreSwimScreen";
|
||||
|
||||
const Stack = createNativeStackNavigator<WorkoutStackParamList>();
|
||||
@@ -26,11 +25,6 @@ export function WorkoutNavigator() {
|
||||
component={BuilderScreen}
|
||||
options={{ title: "Build Workout" }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Templates"
|
||||
component={TemplatesScreen}
|
||||
options={{ title: "Templates" }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="PreSwim"
|
||||
component={PreSwimScreen}
|
||||
|
||||
@@ -6,7 +6,6 @@ export type RootStackParamList = {
|
||||
PreSwim: { workoutId: string };
|
||||
PostSwim: { workoutId: string };
|
||||
SessionDetail: { sessionId: string };
|
||||
Stats: undefined;
|
||||
};
|
||||
|
||||
export type MainTabParamList = {
|
||||
@@ -19,14 +18,12 @@ export type MainTabParamList = {
|
||||
export type WorkoutStackParamList = {
|
||||
WorkoutList: undefined;
|
||||
Builder: { templateId?: string } | undefined;
|
||||
Templates: undefined;
|
||||
PreSwim: { workoutId: string };
|
||||
};
|
||||
|
||||
export type HistoryStackParamList = {
|
||||
HistoryList: undefined;
|
||||
SessionDetail: { sessionId: string };
|
||||
Stats: undefined;
|
||||
};
|
||||
|
||||
export type RootStackProps<T extends keyof RootStackParamList> =
|
||||
|
||||
Reference in New Issue
Block a user