The Hintsa mobile (health) app offers digital coaching plans
created by top performance coaches, combining the unique Hintsa
methodology with years of experience coaching at the elite athlete
and senior executive levels.
Hintsa app official web page
It's a neat package of various health programs and other
professional advice, tailored to the specific needs of each user.
The focus is on the improvement of the physical and cognitive
performance of the individual.
This app is currently on the market for both android and ios.
I was given numerous front-end related tasks. Among them were:
- adding new screens, following specific design guidelines and
making sure they are properly supported on all relevant devices
(including accessibility modes)
- applying animation and other special effects for additional
flavour
- solving challenging issues related to the use of third-party
libraries.
- fixing various bugs that the QA team has reported
Code snippet from ActivityLibrary screen - functional component
...
interface Props extends IBoxProps {
contentCategories: LibraryCategory[]
}
export default function ActivityLibrary({ contentCategories, ...props }: Props) {
const dispatch = useAppDispatch()
const isVisible = useIsFocused()
const [categories, setCategories] = useState<LibraryCategory[]>([])
const [selectedCategory, setSelectedCategory] = useState<number>(0)
useEffect(() => {
isVisible && getUserActivities()
}, [isVisible, contentCategories])
const getUserActivities = async () => {
await dispatch(getAllUserActivities())
setCategories(contentCategories)
}
const currentCategory: LibraryCategory = categories[selectedCategory] ?? []
const subCategories: LibrarySubCategory[] = currentCategory?.subCategories ?? []
if (!categories?.length) return <ActivityLibrarySkeleton />
return (
<VStack h="full" w="full" {...props}>
<CategoryLibraryBar
categories={categories}
my="4"
selectedCategory={selectedCategory}
onSelectedCategory={setSelectedCategory}
/>
<ActivityCarousel subCategories={subCategories} />
</VStack>
)
}
Code snippet from ActivityCarousel - subcomponent of ActivityLibrary
...
interface Props {
subCategories: LibrarySubCategory[]
}
const isIOS = Platform.OS === 'ios'
const isWeb = Platform.OS === 'web'
export default function ActivityCarousel({ subCategories }: Props) {
const renderActivityItem = useCallback(
({ item }: ListRenderItemInfo<UserActivityModel>) => {
const imageSource = item?.activity?.background?.contentUrl
const imageBgColor = item?.activity?.background?.color
const name = item?.activity?.name ?? ''
const description = item?.activity?.description ?? ''
const userActivitySk = item?.sk ?? ''
return (
<Link asChild href={`/activity-selector/${userActivitySk}`}>
<Pressable
key={userActivitySk}
bg="#FFFFFF"
borderColor="gray.50"
borderWidth={1}
overflow="hidden"
p="4"
rounded="lg"
w={isWeb ? '72' : widthPercentageToDP(isIOS ? '77%' : '75%')}
>
<VStack space="2">
<AspectRatio ratio={16 / 9} w="full">
<LazyImage bgColor={imageBgColor} h="full" overflow="hidden" rounded="md" src={imageSource} w="full">
<ActivityStateBadge bottom="-8" left="-8" position="absolute" state={item?.state} />
</LazyImage>
</AspectRatio>
<Text testID={tID(`activity_${name}`)}>{name}</Text>
<Text color="darkTextSecondary" fontSize="sm" numberOfLines={2}>
{description}
</Text>
</VStack>
</Pressable>
</Link>
)
},
[subCategories]
)
const renderCarouselCategory = useCallback((subCategory: LibrarySubCategory, index: number) => {
const activities = subCategory?.activities ?? []
const subCatName = subCategory?.name ?? ''
return (
<VStack key={`${subCatName}_${index}`} py="2">
<PresenceTransition animate={{ opacity: 1, transition: { duration: 500 } }} initial={{ opacity: 0 }} visible>
<Heading mx="4" size="h4">
{subCatName}
</Heading>
</PresenceTransition>
<AnimatedBox
entering={FadeInRight.delay((index + 1) * 200)
.duration(300)
.springify()
.mass(2)}
>
<FlatList
contentContainerStyle={activityContainerStyle}
data={activities}
horizontal
keyExtractor={keyExtractor}
removeClippedSubviews
renderItem={renderActivityItem}
showsHorizontalScrollIndicator={false}
/>
</AnimatedBox>
</VStack>
)
}, [])
return (
<ScrollView contentContainerStyle={parentContainerStyle} h="full" showsVerticalScrollIndicator={false}>
{subCategories.map(renderCarouselCategory)}
</ScrollView>
)
}
const keyExtractor = (item: UserActivityModel, index: number) => `${item?.sk}${index}`
const AnimatedBox = Animated.createAnimatedComponent(Box)
const parentContainerStyle: StyleProp<ViewStyle> = { gap: 8 }
const activityContainerStyle: StyleProp<ViewStyle> = { gap: 18, paddingVertical: 8, paddingHorizontal: 16 }
Code snippet from AudioPlayback reusable component - used for all
audio playback with controls
...
export const AudioPlayback = ({ recording, uri, ...props }: Props) => {
const startMedia = useMediaManager()
const [playbackStatus, setPlaybackStatus] = useState<AVPlaybackStatus>()
const [sound, setSound] = useState<Audio.Sound | undefined>()
const [hasFinished, setFinished] = useState(false)
const [totalDuration, setTotalDuration] = useState(getMinutesSecondsFromMilliseconds(0))
const [elapsedTime, setElapsedTime] = useState<string>('00:00')
const positionMillisRef = useRef<number>(0)
const wasPlayingBeforeDraggingRef = useRef<boolean>(false)
useEffect(() => {
if (playbackStatus?.isLoaded && playbackStatus?.positionMillis > 0) {
positionMillisRef.current = playbackStatus?.positionMillis
}
if (hasFinished) {
positionMillisRef.current = 0
}
}, [playbackStatus, hasFinished])
useEffect(() => {
async function loadSound() {
if (recording?.sound) {
recording.sound.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate)
setSound(recording.sound)
setTotalDuration(recording.duration)
return
}
if (uri) {
const { sound } = await Audio.Sound.createAsync(
{ uri },
{ androidImplementation: 'MediaPlayer' }, // needed for proper playback on Android
onPlaybackStatusUpdate
)
setSound(sound)
}
}
loadSound()
}, [recording?.sound, uri])
const onPlaybackStatusUpdate = useCallback(
(playback: AVPlaybackStatus) => {
if (!playback.isLoaded) return
if (!hasFinished) setPlaybackStatus(playback)
// Update total duration
const durationMillis = playback.durationMillis || 0
if (durationMillis) {
setTotalDuration(getMinutesSecondsFromMilliseconds(durationMillis))
}
// Update elapsed time.
const elapsedMillis = playback?.positionMillis || 0
if (elapsedMillis) {
setElapsedTime(getMinutesSecondsFromMilliseconds(elapsedMillis))
}
if (Math.ceil(playback?.positionMillis) >= Math.floor(durationMillis) || playback.didJustFinish) {
setFinished(true)
}
},
[hasFinished]
)
const onPlay = useCallback(async () => {
if (!playbackStatus?.isLoaded) return
if (!playbackStatus.isPlaying) {
startMedia(sound)
// The sound is currently paused
if (hasFinished) {
// The sound has finished playing
setFinished(false)
setPlaybackStatus(status => (status?.isLoaded ? { ...status, positionMillis: 0 } : status))
sound?.playFromPositionAsync(0) // Play from the start
} else {
// The sound is paused in the middle
sound?.playFromPositionAsync(positionMillisRef.current) // Play from the current position
}
} else {
await sound?.pauseAsync() // Pause the sound
}
}, [playbackStatus, sound, hasFinished])
const onSeek = useCallback(
async (value: number) => {
const wasPlaying = !!(playbackStatus?.isLoaded && playbackStatus?.isPlaying)
if (wasPlaying) {
await sound?.pauseAsync() // Ensure audio is paused before seeking
}
positionMillisRef.current = value
setElapsedTime(getMinutesSecondsFromMilliseconds(value))
if (wasPlaying) {
await sound?.playFromPositionAsync(value) // Only play if it was originally playing
}
},
[playbackStatus?.isLoaded, playbackStatus?.isLoaded && playbackStatus?.isPlaying, sound]
)
const isPlaying = (playbackStatus?.isLoaded && playbackStatus?.isPlaying) || false
const sliderMaxValue = (playbackStatus?.isLoaded && playbackStatus?.durationMillis) || 1000
return (
<HStack alignItems="center" p="2" rounded="full" space="4" {...props}>
<IconButton
_icon={{ color: 'white', as: MaterialIcons, name: isPlaying ? 'pause' : 'play-arrow', size: 'sm' }}
bg="black"
rounded="full"
size="10"
onPress={onPlay}
/>
<Slider
defaultValue={positionMillisRef.current}
flex="1"
flexDir="row"
maxValue={sliderMaxValue}
minValue={0}
value={positionMillisRef.current}
onChange={onSeek}
>
<Slider.Track>
<Slider.FilledTrack />
</Slider.Track>
<Slider.Thumb
size="5"
onResponderEnd={() => {
// On stop dragging, only resume audio if it was playing before dragging
if (wasPlayingBeforeDraggingRef.current) {
sound?.playFromPositionAsync(positionMillisRef.current)
}
}}
onResponderStart={() => {
wasPlayingBeforeDraggingRef.current = (playbackStatus?.isLoaded && playbackStatus?.isPlaying) || false
// Pause the audio when dragging the slider, to handle control over to the user
sound?.pauseAsync()
}}
onStartShouldSetResponder={() => true}
/>
</Slider>
<Text fontFamily="mono">{positionMillisRef.current === 0 ? totalDuration : elapsedTime}</Text>
</HStack>
)
}
Back to projects