Skip to content

Instantly share code, notes, and snippets.

@MannyGozzi
Created May 25, 2024 19:04
Show Gist options
  • Save MannyGozzi/4a7c8ce68965198a8937beeb5f2fd459 to your computer and use it in GitHub Desktop.
Save MannyGozzi/4a7c8ce68965198a8937beeb5f2fd459 to your computer and use it in GitHub Desktop.
Workout Routine Screen For AI Fitness App
import { BORDER_WIDTH } from '@/constants/theme'
import { useTheme } from '@/hooks/useTheme'
import { t } from 'i18next'
import LottieView from 'lottie-react-native'
import React from 'react'
import { Text, View } from 'react-native'
import { TouchableOpacity } from 'react-native-gesture-handler'
import {
FireIcon,
HandThumbDownIcon,
HandThumbUpIcon
} from 'react-native-heroicons/solid'
import Animated, { FadeInUp } from 'react-native-reanimated'
import ChipStat from './ChipStat'
import { CollapsableContainer } from './CollapsableContainer'
import ProgressBar from './ProgressBar'
import { CardState } from './task'
interface IExercise {
name: string
description?: string
sets?: string[]
}
/**
* @state the state of the card, active, inactive, or completed. Closed on inactive.
* @order the index of the card for animation staggering purposes
*/
type ExerciseCardProps = {
item: IExercise
description?: string
state?: CardState
progress?: number
onCompleted?: () => void
setIndex?: number
order?: number
}
const ExerciseCard = ({
item,
state = CardState.INACTIVE,
setIndex = 0,
order = 0,
progress = 0.5
}: ExerciseCardProps) => {
const CUTOFF_LENGTH = 19
const theme = useTheme()
const iconSize = 20.0
const activeSetIndex = state === CardState.ACTIVE ? setIndex : 0
return (
<View
className="rounded-2xl p-2 px-4 mt-2"
style={{
backgroundColor: theme.fg,
borderWidth: BORDER_WIDTH,
borderColor: theme.border
}}
>
<CollapsableContainer expanded={state == CardState.ACTIVE}>
<View
className="w-full p-4 my-1 mb-4 rounded-2xl h-[200px] flex items-center flex-grow justify-center"
style={{
backgroundColor: theme.bg
}}
>
<LottieView
source={require('../../assets/lottie/fire.json')}
autoPlay
loop
style={{ width: 100, height: 100 }}
/>
<Text
style={{
color: theme.text
}}
>
{t('video_coming_soon')}
</Text>
</View>
</CollapsableContainer>
<View className="flex flex-row my-1 justify-between items-center space-x-4 overflow-hidden">
<View className="flex flex-col">
{item.sets
?.slice(activeSetIndex, activeSetIndex + 1)
.map((setStr, index) => {
const [setCount, setWeight] = setStr.split('-')
return (
<Animated.View key={index}>
<ChipStat
name={setCount}
val={setWeight ? setWeight + t('weightUnit') : ''}
/>
</Animated.View>
)
})}
</View>
<Animated.Text
entering={FadeInUp.duration(700).delay(200 * order)}
className="text-lg font-extrabold"
style={{ color: theme.text }}
>
{item.name.length > CUTOFF_LENGTH
? item.name.substring(0, CUTOFF_LENGTH) + '...'
: item.name}
</Animated.Text>
</View>
<CollapsableContainer expanded={state == CardState.ACTIVE}>
<Text
className="text-md font-medium"
style={{
color: theme.text,
padding: 4
}}
>
{item.description}
</Text>
{/* Button Controls */}
<View className="flex flex-row justify-between items-center w-full">
{/* make buttons hidden until icon is clicked */}
<ProgressBar progress={progress} />
<View className="flex flex-row items-center space-x-2 ml-2 mt-1">
<TouchableOpacity
className="rounded-full p-2"
style={{
borderWidth: BORDER_WIDTH,
borderColor: theme.border,
padding: 4
}}
>
<HandThumbDownIcon size={iconSize} fill={'rgb(248 113 113)'} />
</TouchableOpacity>
<TouchableOpacity
className="rounded-full"
style={{
borderWidth: BORDER_WIDTH,
backgroundColor: theme.text,
padding: 4
}}
>
<FireIcon size={iconSize} color={theme.fg} />
</TouchableOpacity>
<TouchableOpacity
className="rounded-full p-2"
style={{
borderWidth: BORDER_WIDTH,
borderColor: theme.border
}}
>
<HandThumbUpIcon size={iconSize} fill={'rgb(74 222 128)'} />
</TouchableOpacity>
</View>
</View>
</CollapsableContainer>
</View>
)
}
export default ExerciseCard
// import AddButton from '@/components/AddButton'
// import AddModal from '@/components/AddModal'
// import { TaskProps } from '@/components/task'
import ExerciseCard from '@/components/ExerciseCard'
import HeaderLgBW from '@/components/HeaderLgBW'
import SafeAreaOffset from '@/components/SafeAreaOffset'
import { AnimShrink, BORDER_WIDTH } from '@/constants/theme'
import useHaptics from '@/hooks/useHaptics'
import { useTheme } from '@/hooks/useTheme'
import { BlurView } from 'expo-blur'
import { t } from 'i18next'
import LottieView from 'lottie-react-native'
import React, { useState } from 'react'
import { Animated, Text, TouchableOpacity, View } from 'react-native'
import { FlatList } from 'react-native-gesture-handler'
import { ArrowLeftIcon, ArrowRightIcon } from 'react-native-heroicons/outline'
import {
FadeInDown,
FadeInUp,
FadeOutDown,
default as ReAnimated
} from 'react-native-reanimated'
import { CardState } from '../components/task'
const RoutineScreen = () => {
// const [addModalVisible, setAddModalVisible] = useState(false)
const [leftScale] = useState(new Animated.Value(1))
const [rightScale] = useState(new Animated.Value(1))
const [congratsScale] = useState(new Animated.Value(1))
const [exercises, setExercises] = useState([
{
name: 'Bench Press',
description: 'Bench Press Description',
sets: ['10-120', '8-130', '6-140']
},
{
name: 'Bicep Curls',
description: 'Bicep Curls Description',
sets: ['10-20', '8-25', '6-30']
},
{
name: 'Squats',
description: 'Squats Description',
sets: ['10-100', '8-110', '6-120']
},
{
name: 'Pullups',
description: 'Deadlifts Description',
sets: ['10', '8', '6']
}
])
const theme = useTheme()
let maxSetCount = exercises[0].sets.length
const [activeCardIndex, setActiveCardIndex] = useState(-1)
const [setIndex, setSetIndex] = useState(0)
const [finished, setFinished] = useState(false)
// const addRoutine = () => {
// tasks.push()
// setAddModalVisible(true)
// }
// const onExitModal = () => {
// setAddModalVisible(false)
// }
// const onTaskAdd = (item: TaskProps) => {
// setTasks([...tasks, item.name])
// }
const next = () => {
AnimShrink(rightScale)
if (finished) return
maxSetCount = exercises[activeCardIndex].sets.length - 1
/* Finished everything */
if (setIndex >= maxSetCount && activeCardIndex >= exercises.length - 1) {
setFinished(true)
setActiveCardIndex(exercises.length)
} else if (setIndex >= maxSetCount) {
/* Finished current exercise */
maxSetCount = exercises[activeCardIndex + 1].sets.length - 1
setActiveCardIndex(prev => Math.min(exercises.length, prev + 1))
setSetIndex(0)
} else {
/* Next set */
setSetIndex(prev => Math.min(maxSetCount, prev + 1))
}
}
const back = () => {
AnimShrink(leftScale)
if (finished) {
setFinished(false)
setActiveCardIndex(prev => prev - 1)
} else if (setIndex <= 0 && activeCardIndex <= 0) {
return
} else if (setIndex <= 0) {
setActiveCardIndex(prev => Math.max(0, prev - 1))
setSetIndex(exercises[activeCardIndex - 1].sets.length - 1)
} else {
setSetIndex(prev => Math.max(0, prev - 1))
}
}
if (activeCardIndex === -1) {
setTimeout(() => {
setActiveCardIndex(0)
}, 300)
}
const getCardState = (index: number, currentIndex: number) => {
if (index > currentIndex) {
return CardState.INACTIVE
} else if (index == currentIndex) {
return CardState.ACTIVE
}
return CardState.COMPLETED
}
return (
<SafeAreaOffset>
<View className="mx-4 pb-12 h-full">
<View className="flex flex-row space-y-2 justify-between items-center pb-4">
<HeaderLgBW>{t('routine')}</HeaderLgBW>
{/* <AddButton addPressed={addRoutine} /> */}
</View>
{!finished && (
<ReAnimated.View
entering={FadeInDown.duration(300)}
exiting={FadeOutDown.duration(300)}
>
<FlatList
data={exercises}
renderItem={({ item, index }) => {
return (
<ExerciseCard
item={item}
key={item.name}
state={getCardState(index, activeCardIndex)}
progress={setIndex / maxSetCount}
setIndex={setIndex}
/>
)
}}
overScrollMode="always"
keyExtractor={item => item.name}
/>
</ReAnimated.View>
)}
{/* <FlatList
data={data}
keyExtractor={item => item.title}
renderItem={({ item }) => (
<SelectableBox
title={item.title}
icon={item.icon}
isSelected={!!selectedItems[item.title]}
onPress={() => handlePress(item.title)}
/>
)}
contentContainerStyle={{
alignItems: 'center',
paddingBottom: 30,
paddingTop: 20
}}
showsVerticalScrollIndicator={false}
numColumns={2}
columnWrapperStyle={
theme.flexWrap ? { justifyContent: 'space-between' } : {}
}
/>
*/}
{/* <AddModal
label={t('add_exercise')}
description={t('add_exercise_desc')}
inputPlaceholder={t('exercise_name')}
visible={addModalVisible}
onExitModal={onExitModal}
onAdd={onTaskAdd}
/> */}
{finished && (
<ReAnimated.View
className="flex justify-center items-center flex-1 overflow-hidden"
entering={FadeInDown.duration(300)}
exiting={FadeInUp.duration(300)}
>
<LottieView
source={require('../../assets/lottie/fire.json')}
autoPlay
loop
style={{ width: 125, height: 125 }}
/>
<TouchableOpacity
style={{
backgroundColor: theme.fg,
borderRadius: 20,
borderWidth: BORDER_WIDTH,
borderColor: theme.border,
overflow: 'hidden',
transform: [{ scale: congratsScale }],
padding: 20,
alignItems: 'center',
marginTop: 20
}}
onPressIn={() => {
AnimShrink(congratsScale)
}}
>
<Text
style={{ color: theme.text, textAlign: 'center' }}
className="text-xl font-bold"
>
{t('routine_complete')}
</Text>
</TouchableOpacity>
</ReAnimated.View>
)}
</View>
<View className="flex w-full flex-row justify-end absolute bottom-[90px] items-end space-x-2 px-4">
<BlurView
intensity={70}
style={{
borderColor: theme.border,
borderWidth: 4
}}
className="rounded-3xl overflow-hidden"
tint={theme.name == 'Dark' ? 'dark' : 'light'}
>
<TouchableOpacity
className="flex flex-row justify-between items-center space-x-4 p-4 m-1"
onPress={back}
onPressIn={() => {
useHaptics()
AnimShrink(leftScale)
}}
style={{ transform: [{ scale: leftScale }] }}
>
<ArrowLeftIcon size={24} color={theme.text} strokeWidth={4} />
</TouchableOpacity>
</BlurView>
<BlurView
intensity={70}
style={{
borderColor: theme.border,
borderWidth: 4
}}
className="rounded-3xl overflow-hidden"
tint={theme.name == 'Dark' ? 'dark' : 'light'}
>
<TouchableOpacity
className="flex flex-row justify-between items-center space-x-4 p-4 m-1"
onPress={next}
onPressIn={() => {
useHaptics()
AnimShrink(rightScale)
}}
style={{ transform: [{ scale: rightScale }] }}
>
<ArrowRightIcon size={24} color={theme.text} strokeWidth={4} />
</TouchableOpacity>
</BlurView>
</View>
</SafeAreaOffset>
)
}
export default RoutineScreen
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment