[ADD] LibCC ChromaCase:

- IconButton and MusicItem creation and documentation
- Update native base theme
This commit is contained in:
mathysPaul
2023-11-02 21:14:38 +01:00
parent e499bb2f9f
commit d2e1ba51c6
18 changed files with 781 additions and 28 deletions

View File

@@ -34,6 +34,7 @@ import SignupView from './views/SignupView';
import PasswordResetView from './views/PasswordResetView'; import PasswordResetView from './views/PasswordResetView';
import ForgotPasswordView from './views/ForgotPasswordView'; import ForgotPasswordView from './views/ForgotPasswordView';
import DiscoveryView from './views/V2/DiscoveryView'; import DiscoveryView from './views/V2/DiscoveryView';
import MusicView from './views/MusicView';
// Util function to hide route props in URL // Util function to hide route props in URL
const removeMe = () => ''; const removeMe = () => '';
@@ -45,6 +46,11 @@ const protectedRoutes = () =>
options: { headerShown: false }, options: { headerShown: false },
link: '/', link: '/',
}, },
Music: {
component: MusicView,
options: { headerShown: false },
link: '/music',
},
HomeNew: { HomeNew: {
component: DiscoveryView, component: DiscoveryView,
options: { headerShown: false }, options: { headerShown: false },
@@ -99,11 +105,6 @@ const protectedRoutes = () =>
const publicRoutes = () => const publicRoutes = () =>
({ ({
// Start: {
// component: StartPageView,
// options: { title: 'Chromacase', headerShown: false },
// link: '/',
// },
Login: { Login: {
component: SigninView, component: SigninView,
options: { title: translate('signInBtn'), headerShown: false }, options: { title: translate('signInBtn'), headerShown: false },

View File

@@ -15,6 +15,7 @@ const ThemeProvider = ({ children }: { children: JSX.Element }) => {
700: 'rgba(255,255,255,0.7)', 700: 'rgba(255,255,255,0.7)',
800: 'rgba(255,255,255,0.8)', 800: 'rgba(255,255,255,0.8)',
900: 'rgba(255,255,255,0.9)', 900: 'rgba(255,255,255,0.9)',
1000: 'rgba(255,255,255,1)',
}; };
const darkGlassmorphism = { const darkGlassmorphism = {
50: 'rgba(16,16,20,0.05)', 50: 'rgba(16,16,20,0.05)',
@@ -27,11 +28,15 @@ const ThemeProvider = ({ children }: { children: JSX.Element }) => {
700: 'rgba(16,16,20,0.7)', 700: 'rgba(16,16,20,0.7)',
800: 'rgba(16,16,20,0.8)', 800: 'rgba(16,16,20,0.8)',
900: 'rgba(16,16,20,0.9)', 900: 'rgba(16,16,20,0.9)',
1000: 'rgba(16,16,20,1)',
}; };
const glassmorphism = colorScheme === 'light' const glassmorphism = colorScheme === 'light'
? lightGlassmorphism ? lightGlassmorphism
: darkGlassmorphism : darkGlassmorphism
const text = colorScheme === 'light'
? darkGlassmorphism
: lightGlassmorphism
return ( return (
<NativeBaseProvider <NativeBaseProvider
@@ -46,6 +51,7 @@ const ThemeProvider = ({ children }: { children: JSX.Element }) => {
mono: 'Lexend', mono: 'Lexend',
}, },
colors: { colors: {
text: text,
primary: { primary: {
50: '#eff1fe', 50: '#eff1fe',
100: '#e7eafe', 100: '#e7eafe',

View File

@@ -18,7 +18,7 @@ const GenreCard = (props: GenreCardProps) => {
<Card shadow={3} onPress={props.onPress}> <Card shadow={3} onPress={props.onPress}>
<VStack m={1.5} space={3} alignItems="center"> <VStack m={1.5} space={3} alignItems="center">
<Box <Box
bg={theme.colors.primary[400]} bg={theme.colors.primary[300]}
w={20} w={20}
h={20} h={20}
borderRadius="full" borderRadius="full"

View File

@@ -10,6 +10,7 @@ import {
} from './ElementTypes'; } from './ElementTypes';
import { ArrowDown2 } from 'iconsax-react-native'; import { ArrowDown2 } from 'iconsax-react-native';
import { useWindowDimensions } from 'react-native'; import { useWindowDimensions } from 'react-native';
import useColorScheme from '../../hooks/colorScheme';
type RawElementProps = { type RawElementProps = {
element: ElementProps; element: ElementProps;
@@ -20,6 +21,9 @@ export const RawElement = ({ element }: RawElementProps) => {
const screenSize = useBreakpointValue({ base: 'small', md: 'big' }); const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
const isSmallScreen = screenSize === 'small'; const isSmallScreen = screenSize === 'small';
const { width: screenWidth } = useWindowDimensions(); const { width: screenWidth } = useWindowDimensions();
const colorScheme = useColorScheme();
const color = colorScheme === 'light' ? 'rgba(0,0,0,0.7)' : 'rgba(255,255,255,0.7)'
return ( return (
<Column <Column
style={{ style={{
@@ -122,7 +126,7 @@ export const RawElement = ({ element }: RawElementProps) => {
case 'custom': case 'custom':
return data; return data;
case 'sectionDropdown': case 'sectionDropdown':
return <ArrowDown2 size="24" color="#fff" variant="Outline" />; return <ArrowDown2 size="24" color={color} variant="Outline" />;
default: default:
return <Text>Unknown type</Text>; return <Text>Unknown type</Text>;
} }

View File

@@ -38,28 +38,28 @@ const ButtonBase: React.FC<ButtonProps> = ({
shadowOpacity: 0.3, shadowOpacity: 0.3,
shadowRadius: 4.65, shadowRadius: 4.65,
elevation: 8, elevation: 8,
backgroundColor: colors.primary[400], backgroundColor: colors.primary[300],
}, },
onHover: { onHover: {
scale: 1.02, scale: 1.02,
shadowOpacity: 0.37, shadowOpacity: 0.37,
shadowRadius: 7.49, shadowRadius: 7.49,
elevation: 12, elevation: 12,
backgroundColor: colors.primary[500], backgroundColor: colors.primary[400],
}, },
onPressed: { onPressed: {
scale: 0.98, scale: 0.98,
shadowOpacity: 0.23, shadowOpacity: 0.23,
shadowRadius: 2.62, shadowRadius: 2.62,
elevation: 4, elevation: 4,
backgroundColor: colors.primary[600], backgroundColor: colors.primary[500],
}, },
Disabled: { Disabled: {
scale: 1, scale: 1,
shadowOpacity: 0.3, shadowOpacity: 0.3,
shadowRadius: 4.65, shadowRadius: 4.65,
elevation: 8, elevation: 8,
backgroundColor: colors.primary[400], backgroundColor: colors.primary[300],
}, },
}); });
@@ -115,7 +115,7 @@ const ButtonBase: React.FC<ButtonProps> = ({
<ActivityIndicator <ActivityIndicator
style={styles.content} style={styles.content}
size="small" size="small"
color={type === 'outlined' ? '#6075F9' : '#FFFFFF'} color={type === 'outlined' ? colors.primary[300] : '#FFFFFF'}
/> />
) : ( ) : (
<View <View
@@ -127,7 +127,7 @@ const ButtonBase: React.FC<ButtonProps> = ({
{icon && ( {icon && (
<MyIcon <MyIcon
size={'18'} size={'18'}
color={type === 'outlined' ? '#6075F9' : colorScheme === 'dark' || type === 'filled' ? '#FFFFFF' : colors.black[500] } color={type === 'outlined' ? colors.primary[300] : colorScheme === 'dark' || type === 'filled' ? '#FFFFFF' : colors.black[500] }
variant={iconVariant} variant={iconVariant}
/> />
)} )}

View File

@@ -56,13 +56,13 @@ const CheckboxBase: React.FC<CheckboxProps> = ({ title, color, style, check, set
{check ? ( {check ? (
<TickSquare <TickSquare
size="24" size="24"
color={color ?? colors.primary[400]} color={color ?? colors.primary[300]}
variant="Bold" variant="Bold"
/> />
) : ( ) : (
<AddSquare <AddSquare
size="24" size="24"
color={color ?? colors.primary[400]} color={color ?? colors.primary[300]}
variant="Outline" variant="Outline"
/> />
)} )}

View File

@@ -0,0 +1,259 @@
// Import required dependencies and components.
import { Icon } from "iconsax-react-native";
import { useRef, useState, useMemo } from "react";
import { Animated, StyleProp, TouchableOpacity, ViewStyle } from "react-native";
// Default values for the component props.
const DEFAULT_SCALE_FACTOR: number = 1.25;
const DEFAULT_ANIMATION_DURATION: number = 250;
const DEFAULT_PADDING: number = 0;
// Define the type for the IconButton props.
type IconButtonProps = {
/**
* Indicates if the button starts in an active state.
* @default false
*/
isActive?: boolean;
/**
* Color of the icon.
*/
color: string;
/**
* Optional color of the icon when active.
*/
colorActive?: string;
/**
* Callback function triggered when the button is pressed.
*/
onPress?: () => void | Promise<void>;
/**
* Size of the icon.
* @default 24
*/
size?: number;
/**
* Icon to display.
*/
icon: Icon;
/**
* Optional icon to display when active.
*/
iconActive?: Icon;
/**
* Variant style of the icon.
* @default "Outline"
*/
variant?: "Outline" | "Bold" | "Bulk" | "Broken" | "TwoTone";
/**
* Variant style of the icon when active.
* @default "Outline"
*/
activeVariant?: "Outline" | "Bold" | "Bulk" | "Broken" | "TwoTone";
/**
* Custom style for the icon.
*/
style?: ViewStyle | ViewStyle[];
/**
* Custom style for the icon's container.
*/
containerStyle?: ViewStyle | ViewStyle[];
/**
* Scale factor when the icon animates on press.
* @default 1.25
*/
scaleFactor?: number;
/**
* Duration of the icon animation in milliseconds.
* @default 250
*/
animationDuration?: number;
/**
* Padding around the icon.
* @default 0
*/
padding?: number;
};
/**
* `IconButton` Component
*
* Render an interactive icon that can toggle between active and inactive states.
* Supports customization of colors, icons, animation speed, size, and more.
*
* Features:
* - Can render two different icons for active and inactive states.
* - Includes an animation that scales the icon when pressed.
* - Supports custom styling for both the icon and its container.
* - Accepts various icon variants for flexibility in design.
*
* Usage:
*
* ```jsx
* <IconButton
* color="#000"
* icon={SomeIcon}
* onPress={() => console.log('Icon pressed!')}
* />
* ```
*
* To use with active states:
*
* ```jsx
* <IconButton
* isActive={true}
* color="#000"
* colorActive="#ff0000"
* icon={SomeIcon}
* iconActive={SomeActiveIcon}
* onPress={() => console.log('Icon toggled!')}
* />
* ```
*
* Note: If `iconActive` is provided but `colorActive` is not,
* the `color` prop will be used for both active and inactive states.
*/
const IconButton: React.FC<IconButtonProps> = ({
isActive = false,
color,
colorActive,
onPress,
size = 24,
icon: Icon,
iconActive: IconActive,
variant = "Outline",
activeVariant = "Outline",
style,
containerStyle,
scaleFactor = DEFAULT_SCALE_FACTOR,
animationDuration = DEFAULT_ANIMATION_DURATION,
padding = DEFAULT_PADDING,
}) => {
// State to track active status.
const [isActiveState, setIsActiveState] = useState<boolean>(isActive);
// Animation values.
const scaleValue: Animated.Value = useRef(new Animated.Value(1)).current;
const animateValue: Animated.Value = useRef(new Animated.Value(isActive ? 0 : 1)).current;
// Check for active icon.
const hasActiveIcon: boolean = !!IconActive;
// Interpolation for icon colors between active and inactive states.
const colorValue: Animated.AnimatedInterpolation<string | number> = animateValue.interpolate({
inputRange: [0, 1],
outputRange: [color, colorActive || color],
});
// Combine styles for the icon container.
const combinedContainerStyle: StyleProp<ViewStyle> = useMemo<(ViewStyle | ViewStyle[] | undefined)[]>(
() => [
{
position: 'relative',
// Adjust width and height to account for specified padding. Since the icons
// are absolutely positioned inside the container, the container's size needs
// to include the padding to ensure the icons are properly centered and spaced.
width: size + (padding * 2),
height: size + (padding * 2),
justifyContent: 'center',
alignItems: 'center',
},
containerStyle
],
[padding, containerStyle]
);
/**
* Toggles the active state of the icon.
* Executes the onPress callback and triggers the animation.
*/
const toggleState = async () => {
// Execute onPress if provided.
if (onPress) {
await onPress();
}
// Toggle isActiveState.
setIsActiveState(!isActiveState);
// Animation sequences.
const animations: Animated.CompositeAnimation[] = [
Animated.sequence([
Animated.timing(scaleValue, {
toValue: scaleFactor,
duration: animationDuration,
useNativeDriver: true,
}),
Animated.timing(scaleValue, {
toValue: 1,
duration: animationDuration,
useNativeDriver: true,
}),
]),
];
if (hasActiveIcon || colorActive) {
animations.push(
Animated.timing(animateValue, {
toValue: isActiveState ? 1 : 0,
duration: animationDuration,
useNativeDriver: true,
})
);
}
// Start animations in parallel.
Animated.parallel(animations).start();
};
return (
<TouchableOpacity
activeOpacity={1}
onPress={toggleState}
style={combinedContainerStyle}
>
{hasActiveIcon && (
<Animated.View
style={[{
padding: padding,
color: colorValue,
opacity: animateValue.interpolate({
inputRange: [0, 1],
outputRange: [1, 0],
}),
position: 'absolute',
transform: [{ scale: scaleValue }],
}, style]}
>
<IconActive size={size} variant={activeVariant}/>
</Animated.View>
)}
<Animated.View
style={[{
padding: padding,
color: colorValue,
opacity: animateValue,
position: 'absolute',
transform: [{ scale: scaleValue }],
}, style]}
>
<Icon size={size} variant={variant} />
</Animated.View>
</TouchableOpacity>
);
};
export default IconButton;

View File

@@ -219,7 +219,7 @@ const InteractiveBase: React.FC<InteractiveBaseProps> = ({
}; };
const animatedStyle = { const animatedStyle = {
backgroundColor: isOutlined ? colors.coolGray[200] : backgroundColorValue, backgroundColor: isOutlined ? colors.coolGray[100] : backgroundColorValue,
borderColor: isOutlined ? backgroundColorValue : 'transparent', borderColor: isOutlined ? backgroundColorValue : 'transparent',
borderWidth: 2, borderWidth: 2,
transform: [{ scale: scaleValue }], transform: [{ scale: scaleValue }],
@@ -229,7 +229,7 @@ const InteractiveBase: React.FC<InteractiveBaseProps> = ({
}; };
const disableStyle = { const disableStyle = {
backgroundColor: isOutlined ? colors.coolGray[200] : styleAnimate.Disabled.backgroundColor, backgroundColor: isOutlined ? colors.coolGray[100] : styleAnimate.Disabled.backgroundColor,
borderColor: isOutlined ? styleAnimate.Disabled.backgroundColor : 'transparent', borderColor: isOutlined ? styleAnimate.Disabled.backgroundColor : 'transparent',
borderWidth: 2, borderWidth: 2,
scale: styleAnimate.Disabled.scale, scale: styleAnimate.Disabled.scale,

View File

@@ -63,7 +63,7 @@ const LinkBase: React.FC<LinkBaseProps> = ({ text, onPress }) => {
<Animated.View style={[ <Animated.View style={[
styles.underline, styles.underline,
{ {
backgroundColor: theme.colors.primary[400], backgroundColor: theme.colors.primary[300],
height: underlineHeight, height: underlineHeight,
opacity: opacity opacity: opacity
} }

View File

@@ -39,7 +39,7 @@ const LogoutButtonCC = ({collapse = false, isGuest = false, buttonType = 'menu',
<ButtonBase <ButtonBase
style={style} style={style}
icon={LogoutCurve} icon={LogoutCurve}
title={collapse ? translate('signOutBtn') : undefined} title={!collapse ? translate('signOutBtn') : undefined}
type={buttonType} type={buttonType}
onPress={async () => {isGuest ? setIsVisible(true) : dispatch(unsetAccessToken());}} onPress={async () => {isGuest ? setIsVisible(true) : dispatch(unsetAccessToken());}}
/> />

View File

@@ -0,0 +1,184 @@
import React, { useMemo, memo } from 'react';
import { StyleSheet } from 'react-native';
import { Column, HStack, Row, Stack, Text, useBreakpointValue, useTheme } from 'native-base';
import { HeartAdd, HeartRemove, Play } from 'iconsax-react-native';
import { Image } from 'react-native';
import IconButton from './IconButton';
import Spacer from '../../components/UI/Spacer';
import { useTranslation } from 'react-i18next';
/**
* Props for the MusicItem component.
*/
interface MusicItemProps {
/** The artist's name. */
artist: string;
/** The song's title. */
song: string;
/** The URL for the song's cover image. */
image: string;
/** The level of the song difficulty . */
level: number;
/** The last score achieved for this song. */
lastScore: number;
/** The highest score achieved for this song. */
bestScore: number;
/** Indicates whether the song is liked/favorited by the user. */
liked: boolean;
/** Callback function triggered when the like button is pressed. */
onLike: () => 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.
*/
const MusicItem: React.FC<MusicItemProps> = memo((props) => {
// 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 formattedLevel = useMemo(() => formatNumber(props.level), [props.level]);
const formattedLastScore = useMemo(() => formatNumber(props.lastScore), [props.lastScore]);
const formattedBestScore = useMemo(() => formatNumber(props.bestScore), [props.bestScore]);
return (
<HStack space={screenSize === 'xl' ? 2 : 1} style={styles.container}>
<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>
{[formattedLevel, formattedLastScore, formattedBestScore].map((value, index) => (
<Text key={index} style={styles.stats}>
{value}
</Text>
))}
</HStack>
);
});
export default MusicItem;

View File

@@ -0,0 +1,93 @@
## IconButton
The `IconButton` is a responsive component displaying an interactive icon. It can toggle between active and inactive states, with customizable animations and styles for each state.
### Features:
- Toggle between active and inactive icons.
- Scale animation on press.
- Full customization of icon colors, sizes, and variants.
- Customizable styles for both the icon and its container.
### Preview
```jsx
<IconButton
color="#000"
icon={SomeIcon}
onPress={() => console.log('Icon pressed!')}
/>
```
With active states:
```jsx
<IconButton
isActive={true}
color="#000"
colorActive="#ff0000"
icon={SomeIcon}
iconActive={SomeActiveIcon}
onPress={() => console.log('Icon toggled!')}
/>
```
### Props
| Prop | Type | Description | Default Value |
|-------------------|--------------|----------------------------------------------------|----------------|
| isActive | boolean | Indicates if the button starts in an active state. | false |
| color | string | Color of the icon. | - |
| colorActive | string | Optional active state color for the icon. | Value of color |
| icon | Component | Icon to display. | - |
| iconActive | Component | Optional icon to display when active. | - |
| variant | string | Icon's variant style. | "Outline" |
| activeVariant | string | Icon's variant style when active. | "Outline" |
| size | number | Size of the icon. | 24 |
| scaleFactor | number | Scale factor for animation. | 1.25 |
| animationDuration | number | Animation duration in milliseconds. | 250 |
| padding | number | Padding around the icon. | 0 |
| onPress | function | Callback triggered on button press. | - |
| style | object/style | Custom style for the icon. | - |
| containerStyle | object/style | Custom style for the icon's container. | - |
## MusicItem
The MusicItem is a responsive component designed to display individual music tracks with key details and interactive buttons, adapting its layout and design across various screen sizes.
### Features:
- Displays artist name, song title, and track cover image.
- Provides interactivity through a play button and a like button.
- Indicates song difficulty level, last score, and best score with automatic number formatting based on user's language preference.
- Optimized for performance, ensuring smooth rendering even in extensive lists.
### Preview
```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!')}
/>
```
### Props
| Prop | Type | Description | Default Value |
|-----------|----------|-------------------------------------------------------|---------------|
| artist | string | Artist's name. | - |
| song | string | Song's title. | - |
| image | string | URL for the song's cover image. | - |
| level | number | Level of the song difficulty. | - |
| lastScore | number | Last score achieved for this song. | - |
| bestScore | number | Highest score achieved for this song. | - |
| liked | boolean | Whether the song is liked/favorited by the user. | false |
| onLike | function | Callback triggered when the like button is pressed. | - |
| onPlay | function | Callback triggered when the song is played. Optional. | - |

View File

@@ -24,7 +24,7 @@ const menu: {
}[] = [ }[] = [
{ type: "main", title: 'menuDiscovery', icon: Discover, link: 'HomeNew' }, { type: "main", title: 'menuDiscovery', icon: Discover, link: 'HomeNew' },
{ type: "main", title: 'menuProfile', icon: User, link: 'User' }, { type: "main", title: 'menuProfile', icon: User, link: 'User' },
{ type: "main", title: 'menuMusic', icon: Music, link: 'Home' }, { type: "main", title: 'menuMusic', icon: Music, link: 'Music' },
{ type: "main", title: 'menuSearch', icon: SearchNormal1, link: 'Search' }, { type: "main", title: 'menuSearch', icon: SearchNormal1, link: 'Search' },
{ type: "main", title: 'menuLeaderBoard', icon: Cup, link: 'Score' }, { type: "main", title: 'menuLeaderBoard', icon: Cup, link: 'Score' },
{ type: "sub", title: 'menuSettings', icon: Setting2, link: 'Settings' }, { type: "sub", title: 'menuSettings', icon: Setting2, link: 'Settings' },
@@ -56,6 +56,7 @@ const ScaffoldCC = ({children, routeName, withPadding = true}: ScaffoldCCProps)
logo={logo} logo={logo}
routeName={routeName} routeName={routeName}
menu={menu} menu={menu}
widthPadding={withPadding}
> >
{children} {children}
</ScaffoldMobileCC> </ScaffoldMobileCC>

View File

@@ -93,14 +93,13 @@ const ScaffoldDesktopCC = (props: ScaffoldDesktopCCProps) => {
}} }}
> >
<View style={!isSmallScreen ? { width: '100%' } : {}}> <View style={!isSmallScreen ? { width: '100%' } : {}}>
<Row space={2} flex={1} style={{ justifyContent: 'center' }}> <Row space={2} flex={1} style={{ justifyContent: isSmallScreen ? 'center' : 'flex-start' }}>
<Image <Image
source={{ uri: props.logo }} source={{ uri: props.logo }}
style={{ style={{
aspectRatio: 1, aspectRatio: 1,
width: 32, width: 32,
height: 32, height: 32,
alignItems: isSmallScreen ? 'center' : 'flex-start',
}} }}
/> />
{!isSmallScreen && {!isSmallScreen &&
@@ -171,7 +170,7 @@ const ScaffoldDesktopCC = (props: ScaffoldDesktopCCProps) => {
/> />
))} ))}
<Spacer height='xs'/> <Spacer height='xs'/>
<LogoutButtonCC collapse={!isSmallScreen} isGuest={props.user.isGuest} style={!isSmallScreen ? { width: '100%' } : {}} buttonType={'menu'}/> <LogoutButtonCC collapse={isSmallScreen} isGuest={props.user.isGuest} style={!isSmallScreen ? { width: '100%' } : {}} buttonType={'menu'}/>
</View> </View>
</View> </View>
<ScrollView <ScrollView

View File

@@ -12,6 +12,7 @@ type ScaffoldMobileCCProps = {
user: User; user: User;
logo: string; logo: string;
routeName: string; routeName: string;
widthPadding: number;
menu: { menu: {
type: "main" | "sub"; type: "main" | "sub";
title: string; title: string;
@@ -32,14 +33,14 @@ const ScaffoldMobileCC = (props: ScaffoldMobileCCProps) => {
<ScrollView <ScrollView
style={{ style={{
flex: 1, flex: 1,
maxHeight: '100vh', maxHeight: '100%',
flexDirection: 'column', flexDirection: 'column',
flexShrink: 0, flexShrink: 0,
padding: 16 padding: props.widthPadding ? 8 : 0
}} }}
contentContainerStyle={{ flex: 1 }} contentContainerStyle={{ flex: 1 }}
> >
<View style={{ flex: 1, minHeight: 'fit-content' }}> <View style={{ flex: 1 }}>
{props.children} {props.children}
</View> </View>
<Spacer/> <Spacer/>

View File

@@ -61,6 +61,12 @@ export const en = {
signinLinkLabel: "You don't have an account? ", signinLinkLabel: "You don't have an account? ",
signinLinkText: 'Sign up for free.', signinLinkText: 'Sign up for free.',
//music
musicTabFavorites: 'Favorites',
musicTabRecentlyPlayed: 'Recently Played',
musicTabStepUp: 'Recommendation',
//search //search
allFilter: 'All', allFilter: 'All',
artistFilter: 'Artists', artistFilter: 'Artists',
@@ -337,6 +343,11 @@ export const fr: typeof en = {
signinLinkLabel: "Vous n'avez pas de compte ? ", signinLinkLabel: "Vous n'avez pas de compte ? ",
signinLinkText: 'Inscrivez-vous gratuitement', signinLinkText: 'Inscrivez-vous gratuitement',
//music
musicTabFavorites : 'Favoris',
musicTabRecentlyPlayed : 'Récemment joué',
musicTabStepUp : 'Recommandation',
//search //search
allFilter: 'Tout', allFilter: 'Tout',
artistFilter: 'Artistes', artistFilter: 'Artistes',
@@ -622,6 +633,11 @@ export const sp: typeof en = {
signinLinkLabel: "¿No tienes una cuenta? ", signinLinkLabel: "¿No tienes una cuenta? ",
signinLinkText: "Regístrate gratis.", signinLinkText: "Regístrate gratis.",
//music
musicTabFavorites: 'Favoritos',
musicTabRecentlyPlayed: 'Jugado recientemente',
musicTabStepUp: 'Recomendación',
//search //search
allFilter: 'Todos', allFilter: 'Todos',
artistFilter: 'Artistas', artistFilter: 'Artistas',

189
front/views/MusicView.tsx Normal file
View File

@@ -0,0 +1,189 @@
import React, { useState } from 'react';
import { Center, HStack, Row, Stack, Text, useBreakpointValue, useTheme } from 'native-base';
import { useWindowDimensions } from 'react-native';
import {
TabView,
SceneMap,
TabBar,
NavigationState,
Route,
SceneRendererProps,
} from 'react-native-tab-view';
import { Heart, Clock, StatusUp, FolderCross, Chart2, ArrowRotateLeft, Cup, Icon } from 'iconsax-react-native';
import { Scene } from 'react-native-tab-view/lib/typescript/src/types';
import useColorScheme from '../hooks/colorScheme';
import { RouteProps } from '../Navigation';
import { translate } from '../i18n/i18n';
import ScaffoldCC from '../components/UI/ScaffoldCC';
import MusicItem from '../components/UI/MusicItem';
interface MusicItemTitleProps {
text: string;
icon: Icon;
isBigScreen: boolean;
}
const MusicItemTitle = (props: MusicItemTitleProps) => {
const colorScheme = useColorScheme();
return (
<Row style={{
display: 'flex',
flex: 1,
maxWidth: props.isBigScreen ? 150 : 50,
height: '100%',
alignItems: 'center',
justifyContent: props.isBigScreen ? 'flex-end' : 'center',
}}>
{props.isBigScreen && <Text fontSize="lg" style={{paddingRight: 8}}>{props.text}</Text>}
<props.icon size={18} color={colorScheme === 'light' ? 'rgba(0,0,0,0.7)' : 'rgba(255,255,255,0.7)'}/>
</Row>
);
}
export const FavoritesMusic = () => {
const { colors } = useTheme();
const screenSize = useBreakpointValue({ base: 'small', md: 'md', xl: 'xl' });
const isSmallScreen = screenSize === 'small';
const isBigScreen = screenSize === 'xl';
return (
<Stack style={{gap: 2, borderRadius: 10, overflow: 'hidden'}}>
<HStack space={isSmallScreen ? 1 : 2} style={{backgroundColor: colors.coolGray[500], paddingHorizontal: isSmallScreen ? 8 : 16, paddingVertical: 12}}>
<Text fontSize="lg" style={{flex: 4, width: '100%', justifyContent: 'center', paddingRight: 60}}>
Song
</Text>
{
[{text: "level", icon: Chart2}, {text: "lastScore", icon: ArrowRotateLeft}, {text: "BastScore", icon: Cup}].map((value, index) => (
<MusicItemTitle key={value.text + "key"} text={value.text} icon={value.icon} isBigScreen={isBigScreen}/>
))
}
</HStack>
<MusicItem
image={"https://static.vecteezy.com/system/resources/previews/016/552/335/non_2x/luffy-kawai-chibi-cute-onepiece-anime-design-and-doodle-art-for-icon-logo-collection-and-others-free-vector.jpg"}
liked={false}
onLike={() => { } }
level={3}
lastScore={25550}
bestScore={420}
artist={'Ludwig van Beethoven'}
song={'Piano Sonata No. 8'}
/>
<MusicItem
image={"https://static.vecteezy.com/system/resources/previews/016/552/335/non_2x/luffy-kawai-chibi-cute-onepiece-anime-design-and-doodle-art-for-icon-logo-collection-and-others-free-vector.jpg"}
liked={true}
onLike={() => { } }
level={3}
lastScore={255500000}
bestScore={42000} artist={'Ludwig van Beethoven'} song={'Sonata for Piano no. 20 in G major, op. 49 no. 2'} />
</Stack>
);
};
export const RecentlyPlayedMusic = () => {
return (
<Center style={{ flex: 1 }}>
<Text>RecentlyPlayedMusic</Text>
</Center>
);
};
export const StepUpMusic = () => {
return (
<Center style={{ flex: 1 }}>
<Text>StepUpMusic</Text>
</Center>
);
};
const renderScene = SceneMap({
favorites: FavoritesMusic,
recentlyPlayed: RecentlyPlayedMusic,
stepUp: StepUpMusic,
});
const getTabData = (key: string) => {
switch (key) {
case 'favorites':
return { index: 0, icon: Heart };
case 'recentlyPlayed':
return { index: 1, icon: Clock };
case 'stepUp':
return { index: 2, icon: StatusUp };
default:
return { index: 3, icon: FolderCross };
}
};
const SetttingsNavigator = (props: RouteProps<{}>) => {
const layout = useWindowDimensions();
const [index, setIndex] = React.useState(0);
const colorScheme = useColorScheme();
const { colors } = useTheme();
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
const isSmallScreen = screenSize === 'small';
const [routes] = React.useState<Route[]>([
{ key: 'favorites', title: 'musicTabFavorites' },
{ key: 'recentlyPlayed', title: 'musicTabRecentlyPlayed' },
{ key: 'stepUp', title: 'musicTabStepUp' },
]);
const renderTabBar = (
props: SceneRendererProps & { navigationState: NavigationState<Route> }
) => (
<TabBar
{...props}
style={{
backgroundColor: 'transparent',
borderBottomWidth: 1,
borderColor: colors.primary[300],
}}
activeColor={ colorScheme === 'light' ? '#000' : '#fff'}
inactiveColor={ colorScheme === 'light' ? 'rgba(0,0,0,0.7)' : 'rgba(255,255,255,0.7)'}
indicatorStyle={{ backgroundColor: colors.primary[300] }}
renderIcon={(
scene: Scene<Route> & {
focused: boolean;
color: string;
}
) => {
const tabHeader = getTabData(scene.route!.key);
return (
<tabHeader.icon size="18" color="#6075F9" variant={scene.focused ? "Bold" : "Outline"} />
);
}}
renderLabel={({ route, color }) =>
layout.width > 800 && (
<Text style={{ color: color, paddingLeft: 10, overflow: 'hidden' }}>
{translate(route.title as 'musicTabFavorites' | 'musicTabRecentlyPlayed' | 'musicTabStepUp')}
</Text>
)
}
tabStyle={{ flexDirection: 'row' }}
/>
);
return (
<ScaffoldCC routeName={props.route.name} withPadding={false}>
<TabView
sceneContainerStyle={{
flex: 1,
alignSelf: 'center',
padding: isSmallScreen ? 4 : 20,
paddingTop: 32,
// maxWidth: 850,
width: '100%'
}}
// style={{ height: 'fit-content' }}
renderTabBar={renderTabBar}
navigationState={{ index, routes }}
renderScene={renderScene}
onIndexChange={setIndex}
initialLayout={{ width: layout.width }}
/>
</ScaffoldCC>
);
};
export default SetttingsNavigator;

View File

@@ -87,11 +87,11 @@ const SetttingsNavigator = (props: RouteProps<{}>) => {
style={{ style={{
backgroundColor: 'transparent', backgroundColor: 'transparent',
borderBottomWidth: 1, borderBottomWidth: 1,
borderColor: colors.primary[500], borderColor: colors.primary[300],
}} }}
activeColor={ colorScheme === 'light' ? '#000' : '#fff'} activeColor={ colorScheme === 'light' ? '#000' : '#fff'}
inactiveColor={ colorScheme === 'light' ? 'rgba(0,0,0,0.7)' : 'rgba(255,255,255,0.7)'} inactiveColor={ colorScheme === 'light' ? 'rgba(0,0,0,0.7)' : 'rgba(255,255,255,0.7)'}
indicatorStyle={{ backgroundColor: colors.primary[500] }} indicatorStyle={{ backgroundColor: colors.primary[300] }}
renderIcon={( renderIcon={(
scene: Scene<Route> & { scene: Scene<Route> & {
focused: boolean; focused: boolean;