Hintsa

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