Skip to content

Commit c47e4a2

Browse files
authored
Merge pull request #1057 from apedley/app-workout-polish
Workout/Activity UI Animations
2 parents f5aea73 + 77706a5 commit c47e4a2

18 files changed

Lines changed: 476 additions & 319 deletions

SparkyFitnessMobile/app.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"expo": {
33
"name": "SparkyFitnessMobile",
44
"slug": "sparkyfitnessmobile",
5-
"version": "1.3.1",
5+
"version": "1.3.2",
66
"scheme": "sparkyfitnessmobile",
77
"orientation": "portrait",
88
"icon": "./assets/icons/appstore.png",

SparkyFitnessMobile/jest.setup.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ jest.mock('react-native-gesture-handler/ReanimatedSwipeable', () => {
168168
jest.mock('react-native-reanimated', () => {
169169
const React = require('react');
170170
const { View } = require('react-native');
171+
const createAnimationMock = () => ({ duration: () => createAnimationMock() });
171172
return {
172173
__esModule: true,
173174
default: { View },
@@ -179,6 +180,9 @@ jest.mock('react-native-reanimated', () => {
179180
withSequence: (...args) => args[args.length - 1],
180181
useAnimatedReaction: jest.fn(),
181182
Easing: { linear: jest.fn(), ease: jest.fn(), bezier: jest.fn(() => jest.fn()) },
183+
FadeIn: createAnimationMock(),
184+
FadeOut: createAnimationMock(),
185+
LinearTransition: createAnimationMock(),
182186
};
183187
});
184188

SparkyFitnessMobile/src/components/AddSheet.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
2-
import { View, Text, Pressable } from 'react-native';
2+
import { View, Text, Pressable, LayoutAnimation } from 'react-native';
33
import {
44
BottomSheetModal,
55
BottomSheetView,
@@ -92,7 +92,14 @@ const AddSheet = React.forwardRef<AddSheetRef, AddSheetProps>(
9292
variant="primary"
9393
className="flex-1 py-5 mx-1.5"
9494
style={{ backgroundColor: raisedBg }}
95-
onPress={() => card.onPress ? handleAction(card.onPress) : setShowExerciseMenu(true)}
95+
onPress={() => {
96+
if (card.onPress) {
97+
handleAction(card.onPress);
98+
} else {
99+
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
100+
setShowExerciseMenu(true);
101+
}
102+
}}
96103
>
97104
<Icon name={card.icon} size={32} color={accentPrimary} />
98105
<Text className="text-text-primary text-sm font-medium mt-2">
@@ -114,11 +121,13 @@ const AddSheet = React.forwardRef<AddSheetRef, AddSheetProps>(
114121
style={{ backgroundColor: raisedBg }}
115122
onPress={() => handleAction(onPress)}
116123
>
117-
<Icon name={icon} size={32} color={accentPrimary} />
124+
<View className="h-10 items-center justify-center">
125+
<Icon name={icon} size={32} color={accentPrimary} />
126+
</View>
118127
<Text className="text-text-primary text-sm font-medium mt-2">
119128
{label}
120129
</Text>
121-
<Text className="text-xs mt-1" style={{ color: textSecondary }}>
130+
<Text className="text-xs mt-1 text-center" numberOfLines={2} style={{ color: textSecondary, minHeight: 32 }}>
122131
{subtitle}
123132
</Text>
124133
</Button>
@@ -138,7 +147,10 @@ const AddSheet = React.forwardRef<AddSheetRef, AddSheetProps>(
138147
<>
139148
<Pressable
140149
className="flex-row items-center mb-3 px-1.5"
141-
onPress={() => setShowExerciseMenu(false)}
150+
onPress={() => {
151+
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
152+
setShowExerciseMenu(false);
153+
}}
142154
>
143155
<Icon name="chevron-back" size={20} color={accentPrimary} />
144156
<Text className="text-sm font-medium ml-1" style={{ color: accentPrimary }}>

SparkyFitnessMobile/src/components/DateNavigator.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ interface DateNavigatorProps {
1515
hideChevrons?: boolean;
1616
showDateAlways?: boolean;
1717
skipSafeAreaTop?: boolean;
18+
skipTopInset?: boolean;
1819
skipHorizontalPadding?: boolean;
1920
}
2021

@@ -28,6 +29,7 @@ const DateNavigator: React.FC<DateNavigatorProps> = ({
2829
hideChevrons,
2930
showDateAlways,
3031
skipSafeAreaTop,
32+
skipTopInset,
3133
skipHorizontalPadding,
3234
}) => {
3335
const insets = useSafeAreaInsets();
@@ -39,8 +41,12 @@ const DateNavigator: React.FC<DateNavigatorProps> = ({
3941
? formatDate(selectedDate)
4042
: formatDateLabel(selectedDate);
4143

44+
const paddingTop = skipTopInset
45+
? 16
46+
: (skipSafeAreaTop && Platform.OS === 'ios') ? 16 : insets.top + 16;
47+
4248
return (
43-
<View style={{ paddingTop: (skipSafeAreaTop && Platform.OS === 'ios') ? 16 : insets.top + 16, paddingHorizontal: skipHorizontalPadding ? 0 : 16 }}
49+
<View style={{ paddingTop, paddingHorizontal: skipHorizontalPadding ? 0 : 16 }}
4450
className="flex-row justify-between items-center pb-5">
4551
<Text className="text-2xl font-bold text-text-primary">{title}</Text>
4652
<View className="flex-row items-center">

SparkyFitnessMobile/src/components/EditableExerciseCard.tsx

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import React, { useMemo } from 'react';
2-
import { View, Text, TouchableOpacity } from 'react-native';
2+
import { Text, TouchableOpacity, View } from 'react-native';
3+
import Animated, { FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated';
34
import { useCSSVariable } from 'uniwind';
45
import Icon from './Icon';
56
import Button from './ui/Button';
67
import SafeImage from './SafeImage';
78
import EditableSetRow from './EditableSetRow';
9+
import { CATEGORY_ICON_MAP } from '../utils/workoutSession';
810
import type { WorkoutDraftExercise } from '../types/drafts';
911
import type { GetImageSource } from '../hooks/useExerciseImageSource';
1012

@@ -47,13 +49,17 @@ function EditableExerciseCard({
4749
() => (imagePath ? getImageSource(imagePath) : null),
4850
[getImageSource, imagePath],
4951
);
52+
const exerciseIcon = (exercise.exerciseCategory && CATEGORY_ICON_MAP[exercise.exerciseCategory]) || 'exercise-weights';
5053

5154
return (
5255
<View className="flex-row items-start py-4">
53-
<SafeImage
54-
source={imageSource}
55-
style={{ width: 48, height: 48, borderRadius: 8, marginRight: 12, marginTop: 2, opacity: 0.8 }}
56-
/>
56+
<View className="mr-3 items-center justify-center" style={{ width: 48, height: 48, marginTop: 2 }}>
57+
<SafeImage
58+
source={imageSource}
59+
style={{ width: 48, height: 48, borderRadius: 8, opacity: 0.8 }}
60+
fallback={<Icon name={exerciseIcon} size={28} color={accentPrimary} />}
61+
/>
62+
</View>
5763
<View className="flex-1">
5864
<View className="flex-row items-center justify-between">
5965
<Text className="text-base font-semibold text-text-primary flex-1 mr-2" numberOfLines={1}>
@@ -76,7 +82,7 @@ function EditableExerciseCard({
7682
) : null}
7783

7884
{exercise.sets.length > 0 && (
79-
<View className="mt-2">
85+
<Animated.View className="mt-2" layout={LinearTransition.duration(300)}>
8086
<View className="flex-row items-center py-1 mb-1">
8187
<Text className="text-xs font-semibold text-text-muted w-10 text-center">Set</Text>
8288
<Text className="text-xs font-semibold text-text-muted flex-1 text-center">Weight</Text>
@@ -88,36 +94,42 @@ function EditableExerciseCard({
8894
const isLastSet = index === exercise.sets.length - 1;
8995
const nextSet = exercise.sets[index + 1];
9096
return (
91-
<EditableSetRow
97+
<Animated.View
9298
key={set.clientId}
93-
exerciseClientId={exercise.clientId}
94-
setClientId={set.clientId}
95-
weight={set.weight}
96-
reps={set.reps}
97-
setNumber={index + 1}
98-
isActive={activeSetKey === setKey}
99-
initialFocusField={activeSetKey === setKey ? activeSetField : undefined}
100-
weightUnit={weightUnit}
101-
nextSetKey={nextSet ? `${exercise.clientId}:${nextSet.clientId}` : null}
102-
onActivateSet={onActivateSet}
103-
onDeactivate={onDeactivateSet}
104-
onUpdateSetField={onUpdateSetField}
105-
onRemoveSet={onRemoveSet}
106-
onAddSet={onAddSet}
107-
isLastSet={isLastSet}
108-
/>
99+
entering={FadeIn.duration(200)}
100+
exiting={FadeOut.duration(150)}
101+
layout={LinearTransition.duration(300)}
102+
>
103+
<EditableSetRow
104+
exerciseClientId={exercise.clientId}
105+
setClientId={set.clientId}
106+
weight={set.weight}
107+
reps={set.reps}
108+
setNumber={index + 1}
109+
isActive={activeSetKey === setKey}
110+
initialFocusField={activeSetKey === setKey ? activeSetField : undefined}
111+
weightUnit={weightUnit}
112+
nextSetKey={nextSet ? `${exercise.clientId}:${nextSet.clientId}` : null}
113+
onActivateSet={onActivateSet}
114+
onDeactivate={onDeactivateSet}
115+
onUpdateSetField={onUpdateSetField}
116+
onRemoveSet={onRemoveSet}
117+
onAddSet={onAddSet}
118+
isLastSet={isLastSet}
119+
/>
120+
</Animated.View>
109121
);
110122
})}
111-
</View>
123+
</Animated.View>
112124
)}
113125

114126
<TouchableOpacity
115-
className="flex-row items-center self-start py-2 mt-1 rounded-lg"
127+
className="flex-row items-center justify-center py-3"
116128
onPress={() => onAddSet(exercise.clientId)}
117129
activeOpacity={0.6}
118130
>
119-
<Icon name="add-circle" size={18} color={accentPrimary} />
120-
<Text className="text-sm font-medium ml-1" style={{ color: accentPrimary }}>
131+
<Icon name="add" size={18} color={accentPrimary} />
132+
<Text className="text-base font-medium ml-1" style={{ color: accentPrimary }}>
121133
Add Set
122134
</Text>
123135
</TouchableOpacity>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import React from 'react';
2+
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
3+
import type { ViewProps } from 'react-native';
4+
import type { AnimatedProps } from 'react-native-reanimated';
5+
6+
type FadeViewProps = AnimatedProps<ViewProps> & {
7+
children: React.ReactNode;
8+
};
9+
10+
const entering = FadeIn.duration(200);
11+
const exiting = FadeOut.duration(150);
12+
13+
const FadeView: React.FC<FadeViewProps> = ({ children, ...props }) => (
14+
<Animated.View entering={entering} exiting={exiting} {...props}>
15+
{children}
16+
</Animated.View>
17+
);
18+
19+
export default FadeView;

SparkyFitnessMobile/src/components/Icon.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const ICON_MAP = {
4343
'globe': { sf: 'globe', ion: 'globe-outline' },
4444

4545
// Food
46-
'food': { sf: 'fork.knife', ion: 'restaurant-outline' },
46+
'food': { sf: 'fork.knife', ion: 'restaurant' },
4747

4848
// Meals
4949
'meal-breakfast': { sf: 'sunrise.fill', ion: 'sunny' },

SparkyFitnessMobile/src/components/WorkoutCard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useCSSVariable } from 'uniwind';
44
import type { ExerciseSessionResponse } from '@workspace/shared';
55
import Icon from './Icon';
66
import SafeImage from './SafeImage';
7-
import { getWorkoutIcon, getSourceLabel, formatDuration, getWorkoutSummary, getFirstImage, buildSessionSubtitle } from '../utils/workoutSession';
7+
import { getWorkoutIcon, getSourceLabel, getWorkoutSummary, getFirstImage, buildSessionSubtitle } from '../utils/workoutSession';
88
import type { GetImageSource } from '../hooks/useExerciseImageSource';
99

1010
interface WorkoutCardProps {
@@ -14,7 +14,7 @@ interface WorkoutCardProps {
1414
distanceUnit?: 'km' | 'miles';
1515
}
1616

17-
export { getSourceLabel, formatDuration, getWorkoutSummary } from '../utils/workoutSession';
17+
export { getSourceLabel, getWorkoutSummary } from '../utils/workoutSession';
1818

1919
const WorkoutCard = React.memo<WorkoutCardProps>(({ session, getImageSource, weightUnit = 'kg', distanceUnit = 'km' }) => {
2020
const accentPrimary = useCSSVariable('--color-accent-primary') as string;

SparkyFitnessMobile/src/components/WorkoutEditableExerciseList.tsx

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
2-
import { View, Text, TouchableOpacity } from 'react-native';
2+
import { Text, TouchableOpacity, View } from 'react-native';
3+
import Animated, { FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated';
34
import { useCSSVariable } from 'uniwind';
45
import Icon from './Icon';
56
import EditableExerciseCard from './EditableExerciseCard';
@@ -45,7 +46,7 @@ function WorkoutEditableExerciseList({
4546
const accentPrimary = useCSSVariable('--color-accent-primary') as string;
4647

4748
return (
48-
<View>
49+
<Animated.View layout={LinearTransition.duration(300)}>
4950
{exercises.map(exercise => {
5051
const imagePath = exercise.images?.[0] ?? null;
5152
const metadataItems = [
@@ -80,21 +81,32 @@ function WorkoutEditableExerciseList({
8081

8182
if (mode === 'detail') {
8283
return (
83-
<View key={exercise.clientId}>
84+
<Animated.View
85+
key={exercise.clientId}
86+
entering={FadeIn.duration(200)}
87+
exiting={FadeOut.duration(150)}
88+
layout={LinearTransition.duration(300)}
89+
>
8490
<View className="border-t border-border-subtle" />
8591
{card}
86-
</View>
92+
</Animated.View>
8793
);
8894
}
8995

9096
return (
91-
<View key={exercise.clientId} className="mb-4">
97+
<Animated.View
98+
key={exercise.clientId}
99+
className="mb-4"
100+
entering={FadeIn.duration(200)}
101+
exiting={FadeOut.duration(150)}
102+
layout={LinearTransition.duration(300)}
103+
>
92104
{card}
93-
</View>
105+
</Animated.View>
94106
);
95107
})}
96108

97-
<View className={mode === 'detail' ? 'py-4' : 'py-4 mb-4'}>
109+
<Animated.View className={mode === 'detail' ? 'py-4' : 'py-4 mb-4'} layout={LinearTransition.duration(300)}>
98110
<TouchableOpacity
99111
className="flex-row items-center self-center py-2 px-3 rounded-lg"
100112
onPress={onAddExercisePress}
@@ -105,8 +117,8 @@ function WorkoutEditableExerciseList({
105117
Add Exercise
106118
</Text>
107119
</TouchableOpacity>
108-
</View>
109-
</View>
120+
</Animated.View>
121+
</Animated.View>
110122
);
111123
}
112124

0 commit comments

Comments
 (0)