Added SongCardInfo for the V2 design and type fixes
This commit is contained in:
@@ -39,10 +39,15 @@ const removeMe = () => '';
|
||||
const protectedRoutes = () =>
|
||||
({
|
||||
Home: {
|
||||
component: TabNavigation,
|
||||
options: { title: translate('welcome'), headerShown: false },
|
||||
component: HomeView,
|
||||
options: { title: translate('welcome'), headerLeft: null },
|
||||
link: '/',
|
||||
},
|
||||
HomeNew: {
|
||||
component: TabNavigation,
|
||||
options: { headerShown: false },
|
||||
link: '/V2',
|
||||
},
|
||||
Play: { component: PlayView, options: { title: translate('play') }, link: '/play/:songId' },
|
||||
Settings: {
|
||||
component: SetttingsNavigator,
|
||||
|
||||
94
front/components/V2/HomeMainSongCard.tsx
Normal file
94
front/components/V2/HomeMainSongCard.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { Image, View } from 'react-native';
|
||||
import { Text, Pressable, PresenceTransition } from 'native-base';
|
||||
|
||||
type HomeMainSongCardProps = {
|
||||
image: string;
|
||||
title: string;
|
||||
artist: string;
|
||||
onPress: () => void;
|
||||
};
|
||||
|
||||
const HomeMainSongCard = (props: HomeMainSongCardProps) => {
|
||||
// on hover darken the image and show the title and artist with fade in
|
||||
return (
|
||||
<Pressable onPress={props.onPress}>
|
||||
{({ isHovered }) => (
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={{
|
||||
uri: props.image,
|
||||
}}
|
||||
style={{
|
||||
aspectRatio: 1,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
flexShrink: 1,
|
||||
}}
|
||||
/>
|
||||
<PresenceTransition
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
}}
|
||||
visible={isHovered}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'flex-start',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 36,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: 'white',
|
||||
fontSize: 46,
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
selectable={false}
|
||||
>
|
||||
{props.title}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
color: 'white',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
selectable={false}
|
||||
>
|
||||
{props.artist}
|
||||
</Text>
|
||||
</View>
|
||||
</PresenceTransition>
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
HomeMainSongCard.defaultProps = {
|
||||
onPress: () => {},
|
||||
};
|
||||
|
||||
export default HomeMainSongCard;
|
||||
259
front/components/V2/SongCardInfo.tsx
Normal file
259
front/components/V2/SongCardInfo.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
import Song from '../../models/Song';
|
||||
import React from 'react';
|
||||
import { Image, View } from 'react-native';
|
||||
import { Pressable, Text, PresenceTransition, Icon, Button } from 'native-base';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
|
||||
type SongCardInfoProps = {
|
||||
song: Song;
|
||||
onPress: () => void;
|
||||
onPlay: () => void;
|
||||
};
|
||||
|
||||
const CardDims = {
|
||||
height: 200,
|
||||
width: 200,
|
||||
};
|
||||
|
||||
const Scores = [
|
||||
{
|
||||
icon: 'warning',
|
||||
score: 3,
|
||||
},
|
||||
{
|
||||
icon: 'star',
|
||||
score: -225,
|
||||
},
|
||||
{
|
||||
icon: 'trophy',
|
||||
score: 100,
|
||||
},
|
||||
];
|
||||
|
||||
const SongCardInfo = (props: SongCardInfoProps) => {
|
||||
const [isPlayHovered, setIsPlayHovered] = React.useState(false);
|
||||
const [isHovered, setIsHovered] = React.useState(false);
|
||||
const [isSlided, setIsSlided] = React.useState(false);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: CardDims.width,
|
||||
height: CardDims.height,
|
||||
// @ts-expect-error boxShadow isn't yet supported by react native
|
||||
boxShadow: '0px 4px 4px 0px rgba(0,0,0,0.25)',
|
||||
backDropFilter: 'blur(2px)',
|
||||
backgroundColor: 'rgba(16, 16, 20, 0.70)',
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
delayHoverIn={7}
|
||||
isHovered={isPlayHovered ? true : undefined}
|
||||
onPress={props.onPress}
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
onHoverIn={() => {
|
||||
setIsHovered(true);
|
||||
}}
|
||||
onHoverOut={() => {
|
||||
setIsHovered(false);
|
||||
setIsSlided(false);
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
width: CardDims.width,
|
||||
height: CardDims.height,
|
||||
backgroundColor: 'rgba(16, 16, 20, 0.7)',
|
||||
borderRadius: 12,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
marginBottom: 8,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{Scores.map((score, idx) => (
|
||||
<View
|
||||
key={score.icon + idx}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
// @ts-expect-error gap isn't yet supported by react native
|
||||
gap: 5,
|
||||
paddingHorizontal: 10,
|
||||
}}
|
||||
>
|
||||
<Icon as={Ionicons} name={score.icon} size={17} color="white" />
|
||||
<Text
|
||||
style={{
|
||||
color: 'white',
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
{score.score}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
<PresenceTransition
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
}}
|
||||
visible={isHovered}
|
||||
initial={{
|
||||
translateY: 0,
|
||||
}}
|
||||
animate={{
|
||||
translateY: -55,
|
||||
}}
|
||||
onTransitionComplete={() => {
|
||||
if (isHovered) {
|
||||
setIsSlided(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: props.song.cover }}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: CardDims.width,
|
||||
height: CardDims.height,
|
||||
borderRadius: 12,
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'flex-start',
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 7,
|
||||
borderRadius: 12,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={{
|
||||
color: 'white',
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
{props.song.name}
|
||||
</Text>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={{
|
||||
color: 'white',
|
||||
fontSize: 12,
|
||||
fontWeight: 'normal',
|
||||
}}
|
||||
>
|
||||
{props.song.artistId}
|
||||
</Text>
|
||||
</View>
|
||||
<Ionicons
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
}}
|
||||
name="bookmark-outline"
|
||||
size={17}
|
||||
color="#6075F9"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</PresenceTransition>
|
||||
{/* icon bu=outon appear in the middle of the card after the first presencetransition is done */}
|
||||
<PresenceTransition
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
}}
|
||||
visible={isSlided}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onHoverIn={() => {
|
||||
setIsPlayHovered(true);
|
||||
}}
|
||||
onHoverOut={() => {
|
||||
setIsPlayHovered(false);
|
||||
}}
|
||||
borderRadius={100}
|
||||
marginBottom={35}
|
||||
leftIcon={
|
||||
<Ionicons
|
||||
name="play-outline"
|
||||
color={'white'}
|
||||
size={20}
|
||||
rounded="sm"
|
||||
/>
|
||||
}
|
||||
onPress={props.onPlay}
|
||||
/>
|
||||
</View>
|
||||
</PresenceTransition>
|
||||
</>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
SongCardInfo.defaultProps = {
|
||||
onPress: () => {},
|
||||
onPlay: () => {},
|
||||
};
|
||||
|
||||
export default SongCardInfo;
|
||||
@@ -66,6 +66,7 @@ const TabNavigation = () => {
|
||||
const child = <HomeView />;
|
||||
|
||||
const appTabs = tabs.map((t) => {
|
||||
// use the same instance of a component between desktop and mobile
|
||||
return {
|
||||
...t,
|
||||
onPress: () => setActiveTab(t.id),
|
||||
@@ -94,7 +95,20 @@ const TabNavigation = () => {
|
||||
activeTabID={activeTab}
|
||||
setActiveTabID={setActiveTab}
|
||||
>
|
||||
{child}
|
||||
<View
|
||||
style={{
|
||||
width: 'calc(100% - 5)',
|
||||
height: '100%',
|
||||
backgroundColor: 'rgba(16, 16, 20, 0.50)',
|
||||
borderRadius: 12,
|
||||
margin: 5,
|
||||
// @ts-expect-error backDropFilter isn't yet supported by react native
|
||||
backDropFilter: 'blur(2px)',
|
||||
padding: 15,
|
||||
}}
|
||||
>
|
||||
{child}
|
||||
</View>
|
||||
</TabNavigationPhone>
|
||||
) : (
|
||||
<TabNavigationDesktop
|
||||
@@ -104,7 +118,21 @@ const TabNavigation = () => {
|
||||
isCollapsed={isDesktopCollapsed}
|
||||
setIsCollapsed={setIsDesktopCollapsed}
|
||||
>
|
||||
{child}
|
||||
<View
|
||||
style={{
|
||||
width: 'calc(100% - 10)',
|
||||
height: '100%',
|
||||
backgroundColor: 'rgba(16, 16, 20, 0.50)',
|
||||
borderRadius: 12,
|
||||
marginVertical: 10,
|
||||
marginRight: 10,
|
||||
// @ts-expect-error backDropFilter isn't yet supported by react native
|
||||
backDropFilter: 'blur(2px)',
|
||||
padding: 20,
|
||||
}}
|
||||
>
|
||||
{child}
|
||||
</View>
|
||||
</TabNavigationDesktop>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { View, Image } from 'react-native';
|
||||
import { Divider, Text, Center } from 'native-base';
|
||||
import { Divider, Text, Center, ScrollView } from 'native-base';
|
||||
import TabNavigationButton from './TabNavigationButton';
|
||||
import TabNavigationList from './TabNavigationList';
|
||||
import { useAssets } from 'expo-asset';
|
||||
@@ -140,10 +140,14 @@ const TabNavigationDesktop = (props: TabNavigationDesktopProps) => {
|
||||
</TabNavigationList>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{
|
||||
height: '100%',
|
||||
width: 'calc(100% - 300px)',
|
||||
}}>{props.children}</View>
|
||||
<ScrollView
|
||||
style={{
|
||||
height: '100%',
|
||||
width: 'calc(100% - 300px)',
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { View } from 'react-native';
|
||||
import { Text, Center } from 'native-base';
|
||||
import { Center, ScrollView } from 'native-base';
|
||||
import TabNavigationButton from './TabNavigationButton';
|
||||
import { NaviTab } from './TabNavigation';
|
||||
|
||||
@@ -55,10 +55,14 @@ const TabNavigationPhone = (props: TabNavigationPhoneProps) => {
|
||||
</View>
|
||||
</Center>
|
||||
</View>
|
||||
<View style={{
|
||||
width: '100%',
|
||||
height: 'calc(100% - 90px)',
|
||||
}}>{props.children}</View>
|
||||
<ScrollView
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 'calc(100% - 90px)',
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -136,6 +136,12 @@ const HomeView = () => {
|
||||
size="sm"
|
||||
onPress={() => navigation.navigate('Settings')}
|
||||
/>
|
||||
<TextButton
|
||||
label={'V2'}
|
||||
colorScheme="gray"
|
||||
size="sm"
|
||||
onPress={() => navigation.navigate('HomeNew')}
|
||||
/>
|
||||
</HStack>
|
||||
<Box style={{ width: '100%' }}>
|
||||
<Heading>
|
||||
|
||||
@@ -1,11 +1,48 @@
|
||||
import { View, Image } from 'react-native';
|
||||
import { Text } from 'native-base';
|
||||
import { View } from 'react-native';
|
||||
import { Text, useBreakpointValue } from 'native-base';
|
||||
import React from 'react';
|
||||
import { useQuery } from '../../Queries';
|
||||
import HomeMainSongCard from '../../components/V2/HomeMainSongCard';
|
||||
import SongCardInfo from '../../components/V2/SongCardInfo';
|
||||
import API from '../../API';
|
||||
|
||||
const bigSideRatio = 100;
|
||||
const smallSideRatio = 61;
|
||||
const bigSideRatio = 1000;
|
||||
const smallSideRatio = 618;
|
||||
|
||||
type HomeCardProps = {
|
||||
image: string;
|
||||
title: string;
|
||||
artist: string;
|
||||
};
|
||||
|
||||
const cards = [
|
||||
{
|
||||
image: 'https://media.discordapp.net/attachments/717080637038788731/1153688155292180560/image_homeview1.png',
|
||||
title: 'Beethoven',
|
||||
artist: 'Synphony No. 9',
|
||||
},
|
||||
{
|
||||
image: 'https://media.discordapp.net/attachments/717080637038788731/1153688154923090093/image_homeview2.png',
|
||||
title: 'Mozart',
|
||||
artist: 'Lieder Kantate KV 619',
|
||||
},
|
||||
{
|
||||
image: 'https://media.discordapp.net/attachments/717080637038788731/1153688154499457096/image_homeview3.png',
|
||||
title: 'Back',
|
||||
artist: 'Truc Truc',
|
||||
},
|
||||
{
|
||||
image: 'https://media.discordapp.net/attachments/717080637038788731/1153688154109394985/image_homeview4.png',
|
||||
title: 'Mozart',
|
||||
artist: 'Machin Machin',
|
||||
},
|
||||
] as [HomeCardProps, HomeCardProps, HomeCardProps, HomeCardProps];
|
||||
|
||||
const HomeView = () => {
|
||||
const songsQuery = useQuery(API.getSongSuggestions);
|
||||
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
||||
const isPhone = screenSize === 'small';
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
@@ -15,19 +52,13 @@ const HomeView = () => {
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
aspectRatio: 16 / 9,
|
||||
}}
|
||||
>
|
||||
<View>
|
||||
<View
|
||||
style={{
|
||||
alignSelf: 'stretch',
|
||||
maxWidth: '1100px',
|
||||
alignItems: 'stretch',
|
||||
flexDirection: 'row',
|
||||
display: 'flex',
|
||||
gap: '0px',
|
||||
flexDirection: isPhone ? 'column' : 'row',
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -35,23 +66,14 @@ const HomeView = () => {
|
||||
flexGrow: bigSideRatio,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={{
|
||||
uri: 'https://media.discordapp.net/attachments/717080637038788731/1153688155292180560/image_homeview1.png',
|
||||
}}
|
||||
style={{
|
||||
aspectRatio: 1,
|
||||
}}
|
||||
/>
|
||||
<HomeMainSongCard {...cards[0]} />
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexGrow: smallSideRatio,
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexDirection: isPhone ? 'row' : 'column',
|
||||
alignItems: 'stretch',
|
||||
gap: '0px',
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -59,23 +81,14 @@ const HomeView = () => {
|
||||
flexGrow: bigSideRatio,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={{
|
||||
uri: 'https://media.discordapp.net/attachments/717080637038788731/1153688154923090093/image_homeview2.png',
|
||||
}}
|
||||
style={{
|
||||
aspectRatio: 1,
|
||||
}}
|
||||
/>
|
||||
<HomeMainSongCard {...cards[1]} />
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexGrow: smallSideRatio,
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row-reverse',
|
||||
flexDirection: isPhone ? 'column-reverse' : 'row-reverse',
|
||||
alignItems: 'stretch',
|
||||
gap: '0px',
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -83,47 +96,32 @@ const HomeView = () => {
|
||||
flexGrow: bigSideRatio,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={{
|
||||
uri: 'https://media.discordapp.net/attachments/717080637038788731/1153688154499457096/image_homeview3.png',
|
||||
}}
|
||||
style={{
|
||||
aspectRatio: 1,
|
||||
}}
|
||||
/>
|
||||
<HomeMainSongCard {...cards[2]} />
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexGrow: smallSideRatio,
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column-reverse',
|
||||
flexDirection: isPhone ? 'row-reverse' : 'column-reverse',
|
||||
alignItems: 'stretch',
|
||||
gap: '0px',
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexGrow: bigSideRatio,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'stretch',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={{
|
||||
uri: 'https://media.discordapp.net/attachments/717080637038788731/1153688154109394985/image_homeview4.png',
|
||||
}}
|
||||
style={{
|
||||
aspectRatio: 1,
|
||||
}}
|
||||
/>
|
||||
<HomeMainSongCard {...cards[3]} />
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
flexGrow: smallSideRatio,
|
||||
}}
|
||||
>
|
||||
<Text>More</Text>
|
||||
</View>
|
||||
></View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -137,7 +135,39 @@ const HomeView = () => {
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Text>Footer</Text>
|
||||
<Text
|
||||
style={{
|
||||
color: 'white',
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
marginLeft: 16,
|
||||
marginBottom: 16,
|
||||
marginTop: 24,
|
||||
}}
|
||||
>
|
||||
{'Suggestions'}
|
||||
</Text>
|
||||
{songsQuery.isLoading && <Text>Loading...</Text>}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'flex-start',
|
||||
// @ts-expect-error - gap is not in the typings
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
{songsQuery.data?.map((song) => (
|
||||
<SongCardInfo
|
||||
key={song.id}
|
||||
song={song}
|
||||
onPress={() => {
|
||||
console.log('SongCardInfo pressed');
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user