Files
Chromacase/front/components/UI/MusicItem.tsx
2024-01-13 01:10:58 +01:00

190 lines
5.6 KiB
TypeScript

/* eslint-disable react/prop-types */
import React, { useMemo, memo } from 'react';
import { StyleSheet, ViewStyle, Image } from 'react-native';
import { Column, HStack, Row, Stack, Text, useBreakpointValue, useTheme } from 'native-base';
import { HeartAdd, HeartRemove, Play } from 'iconsax-react-native';
import IconButton from './IconButton';
import Spacer from '../../components/UI/Spacer';
import { useTranslation } from 'react-i18next';
/**
* Props for the MusicItem component.
*/
export interface MusicItemType {
/** The artist's name. */
artist: string;
/** The song's title. */
song: string;
/** The URL for the song's cover image. */
image: string;
/** The last score achieved for this song. */
lastScore: number | null | undefined;
/** The highest score achieved for this song. */
bestScore: number | null | undefined;
/** Indicates whether the song is liked/favorited by the user. */
liked: boolean;
/** Custom style for the container. */
style?: ViewStyle | ViewStyle[];
/** Callback function triggered when the like button is pressed. */
onLike: (state: boolean) => void;
/** Callback function triggered when the song is played. */
onPlay: () => void;
}
// Custom hook to handle the number formatting based on the current user's language.
function useNumberFormatter() {
const { i18n } = useTranslation();
// Memoizing the number formatter to avoid unnecessary recalculations.
// It will be recreated only when the language changes.
const formatter = useMemo(() => {
return new Intl.NumberFormat(i18n.language, {
notation: 'compact',
compactDisplay: 'short',
});
}, [i18n.language]);
return (num: number) => formatter.format(num);
}
/**
* `MusicItem` Component
*
* Display individual music tracks with artist information, cover image, song title, and associated stats.
* Designed for optimal performance and responsiveness across different screen sizes.
*
* Features:
* - Displays artist name, song title, and track cover image.
* - Indicates user interaction with a like/favorite feature.
* - Provides a play button for user interaction.
* - Adapts its layout and design responsively according to screen size.
* - Optimized performance to ensure smooth rendering even in long lists.
* - Automatic number formatting based on user's language preference.
*
* Usage:
*
* ```jsx
* <MusicItem
* artist="John Doe"
* song="A Beautiful Song"
* image="https://example.com/image.jpg"
* level={5}
* lastScore={3200}
* bestScore={5000}
* liked={true}
* onLike={() => {() => console.log('Music liked!')}}
* onPlay={() => {() => console.log('Play music!')}
* />
* ```
*
* Note:
* - The number formatting for `level`, `lastScore`, and `bestScore` adapts automatically based on the user's language preference using the i18n module.
* - Given its optimized performance characteristics, this component is suitable for rendering in lists with potentially hundreds of items.
*/
function MusicItemComponent(props: MusicItemType) {
// Accessing theme colors and breakpoint values for responsive design
const { colors } = useTheme();
const screenSize = useBreakpointValue({ base: 'small', md: 'md', xl: 'xl' });
const formatNumber = useNumberFormatter();
// Styles are memoized to optimize performance.
const styles = useMemo(
() =>
StyleSheet.create({
container: {
backgroundColor: colors.coolGray[500],
paddingRight: screenSize === 'small' ? 8 : 16,
},
playButtonContainer: {
zIndex: 1,
position: 'absolute',
right: -8,
bottom: -6,
},
playButton: {
backgroundColor: colors.primary[300],
borderRadius: 999,
},
image: {
position: 'relative',
width: screenSize === 'xl' ? 80 : 60,
height: screenSize === 'xl' ? 80 : 60,
},
artistText: {
color: colors.text[700],
fontWeight: 'bold',
},
songContainer: {
width: '100%',
},
stats: {
display: 'flex',
flex: 1,
maxWidth: screenSize === 'xl' ? 150 : 50,
height: '100%',
alignItems: 'center',
justifyContent: screenSize === 'xl' ? 'flex-end' : 'center',
},
}),
[colors, screenSize]
);
// Memoizing formatted numbers to avoid unnecessary computations.
const formattedLastScore = useMemo(() => formatNumber(props.lastScore ?? 0), [props.lastScore]);
const formattedBestScore = useMemo(() => formatNumber(props.bestScore ?? 0), [props.bestScore]);
return (
<HStack space={screenSize === 'xl' ? 2 : 1} style={[styles.container, props.style]}>
<Stack style={{ position: 'relative', overflow: 'hidden' }}>
<IconButton
containerStyle={styles.playButtonContainer}
style={styles.playButton}
padding={8}
onPress={props.onPlay}
color="#FFF"
icon={Play}
variant="Bold"
size={24}
/>
<Image source={{ uri: props.image }} style={styles.image} />
</Stack>
<Column style={{ flex: 4, width: '100%', justifyContent: 'center' }}>
<Text numberOfLines={1} style={styles.artistText}>
{props.artist}
</Text>
{screenSize === 'xl' && <Spacer height="xs" />}
<Row space={2} style={styles.songContainer}>
<Text numberOfLines={1}>{props.song}</Text>
<IconButton
colorActive={colors.text[700]}
color={colors.primary[300]}
icon={HeartAdd}
iconActive={HeartRemove}
activeVariant="Bold"
size={screenSize === 'xl' ? 24 : 18}
isActive={props.liked}
onPress={props.onLike}
/>
</Row>
</Column>
{[formattedLastScore, formattedBestScore].map((value, index) => (
<Text key={index} style={styles.stats}>
{value}
</Text>
))}
</HStack>
);
}
const MusicItem = memo(MusicItemComponent);
export default MusicItem;