fixing error from CI

This commit is contained in:
mathysPaul
2023-09-20 17:59:36 +02:00
31 changed files with 1260 additions and 24 deletions
+1 -1
View File
@@ -8,6 +8,6 @@ insert_final_newline = true
indent_style = tab
indent_size = tab
[{*.yaml,*.yml}]
[{*.yaml,*.yml,*.nix}]
indent_style = space
indent_size = 2
+2 -2
View File
@@ -10,8 +10,8 @@
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:dev": "nest start --watch --preserveWatchOutput",
"start:debug": "nest start --debug --watch --preserveWatchOutput",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
+3 -1
View File
@@ -97,7 +97,9 @@ export class AuthController {
@UseGuards(JwtAuthGuard)
@Put('reverify')
async reverify(@Request() req: any): Promise<void> {
await this.authService.sendVerifyMail(req.user);
const user = await this.usersService.user({ id: req.user.id });
if (!user) throw new BadRequestException("Invalid user");
await this.authService.sendVerifyMail(user);
}
@ApiBody({ type: LoginDto })
+1
View File
@@ -37,6 +37,7 @@ export class AuthService {
async sendVerifyMail(user: User) {
if (process.env.IGNORE_MAILS === "true") return;
console.log("Sending verification mail to", user.email);
const token = await this.jwtService.signAsync(
{
userId: user.id,
+18 -1
View File
@@ -40,13 +40,14 @@ services:
retries: 5
ports:
- "5432:5432"
front:
build:
context: ./front
dockerfile: Dockerfile.dev
environment:
- SCOROMETER_URL=http://scorometer:6543/
- NGINX_PORT=80
- NGINX_PORT=4567
ports:
- "19006:19006"
volumes:
@@ -55,3 +56,19 @@ services:
- "back"
env_file:
- .env
nginx:
image: nginx
environment:
- API_URL=http://back:3000
- SCOROMETER_URL=http://scorometer:6543
- FRONT_URL=http://front:19006
- PORT=4567
depends_on:
- back
- front
volumes:
- "./front/assets:/assets:ro"
- "./front/nginx.conf.template.dev:/etc/nginx/templates/default.conf.template:ro"
ports:
- "4567:4567"
+1 -5
View File
@@ -35,11 +35,7 @@ services:
retries: 5
front:
build:
context: ./front
args:
- API_URL=${API_URL}
- SCORO_URL=${SCORO_URL}
build: ./front
environment:
- API_URL=http://back:3000/
- SCOROMETER_URL=http://scorometer:6543/
+4 -1
View File
@@ -1,4 +1,7 @@
node_modules/
.expo/
.idea/
.vscode/
.vscode/
.dockerignore
Dockerfile
Dockerfile.dev
+2 -3
View File
@@ -22,6 +22,5 @@ RUN yarn tsc && expo build:web
# Serve the app
FROM nginx:1.21-alpine
COPY --from=build /app/web-build /usr/share/nginx/html
COPY nginx.conf.template /etc/nginx/conf.d/default.conf.template
CMD envsubst '$API_URL $SCOROMETER_URL $NGINX_PORT' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'
COPY ./assets/ /usr/share/nginx/html/assets/
COPY nginx.conf.template /etc/nginx/templates/default.conf.template
+6
View File
@@ -32,6 +32,7 @@ import GoogleView from './views/GoogleView';
import VerifiedView from './views/VerifiedView';
import SigninView from './views/SigninView';
import SignupView from './views/SignupView';
import TabNavigation from './components/V2/TabNavigation';
// Util function to hide route props in URL
const removeMe = () => '';
@@ -43,6 +44,11 @@ const protectedRoutes = () =>
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,
Binary file not shown.
+37
View File
@@ -0,0 +1,37 @@
import { useEffect, useRef } from 'react';
import { Slider, Switch, Text, View } from 'native-base';
export const Metronome = ({ paused = false, bpm }: { paused?: boolean; bpm: number }) => {
const ref = useRef<HTMLAudioElement | null>(null);
const enabled = useRef<boolean>(false);
const volume = useRef<number>(50);
useEffect(() => {
if (paused) return;
const int = setInterval(() => {
if (!enabled.current) return;
if (!ref.current) ref.current = new Audio('/assets/metronome.mp3');
ref.current.volume = volume.current / 100;
ref.current.play();
}, 60000 / bpm);
return () => clearInterval(int);
}, [bpm, paused]);
return (
<View>
<Text>Metronome Settings</Text>
<Text>Enabled:</Text>
<Switch value={enabled.current} onToggle={() => (enabled.current = !enabled.current)} />
<Text>Volume:</Text>
<Slider
maxWidth={'500px'}
value={volume.current}
onChange={(x) => (volume.current = x)}
>
<Slider.Track>
<Slider.FilledTrack />
</Slider.Track>
<Slider.Thumb />
</Slider>
</View>
);
};
+3
View File
@@ -6,6 +6,7 @@ import { PianoCursorPosition } from '../models/PianoGame';
type PartitionCoordProps = {
// The Buffer of the MusicXML file retreived from the API
file: string;
bpmRef: React.MutableRefObject<number>;
onPartitionReady: () => void;
onEndReached: () => void;
onResume: () => void;
@@ -18,6 +19,7 @@ const PartitionCoord = ({
onEndReached,
onPause,
onResume,
bpmRef,
}: PartitionCoordProps) => {
const [partitionData, setPartitionData] = React.useState<
[[number, number], string, PianoCursorPosition[]] | null
@@ -28,6 +30,7 @@ const PartitionCoord = ({
{!partitionData && (
<PartitionView
file={file}
bpmRef={bpmRef}
onPartitionReady={(dims, base64data, a) => {
setPartitionData([dims, base64data, a]);
onPartitionReady();
+3 -1
View File
@@ -1,7 +1,7 @@
/* eslint-disable no-mixed-spaces-and-tabs */
// Inspired from OSMD example project
// https://github.com/opensheetmusicdisplay/react-opensheetmusicdisplay/blob/master/src/lib/OpenSheetMusicDisplay.jsx
import React, { useEffect } from 'react';
import React, { MutableRefObject, useEffect } from 'react';
import {
CursorType,
Fraction,
@@ -19,6 +19,7 @@ type PartitionViewProps = {
base64data: string,
cursorInfos: PianoCursorPosition[]
) => void;
bpmRef: MutableRefObject<number>;
onEndReached: () => void;
// Timestamp of the play session, in milisecond
timestamp: number;
@@ -62,6 +63,7 @@ const PartitionView = (props: PartitionViewProps) => {
_osmd.render();
_osmd.cursor.show();
const bpm = _osmd.Sheet.HasBPMInfo ? _osmd.Sheet.getExpressionsStartTempoInBPM() : 60;
props.bpmRef.current = bpm;
const wholeNoteLength = Math.round((60 / bpm) * 4000);
const curPos = [];
while (!_osmd.cursor.iterator.EndReached) {
+98
View File
@@ -0,0 +1,98 @@
import { Image, View } from 'react-native';
import { Text, Pressable, PresenceTransition } from 'native-base';
type HomeMainSongCardProps = {
image: string;
title: string;
artist: string;
fontSize: number;
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: props.fontSize,
fontWeight: 'bold',
}}
numberOfLines={2}
selectable={false}
>
{props.title}
</Text>
<Text
style={{
color: 'white',
fontSize: props.fontSize * 0.4,
fontWeight: 'bold',
textAlign: 'center',
}}
numberOfLines={1}
selectable={false}
>
{props.artist}
</Text>
</View>
</PresenceTransition>
</View>
)}
</Pressable>
);
};
HomeMainSongCard.defaultProps = {
onPress: () => {},
fontSize: 16,
};
export default HomeMainSongCard;
+279
View File
@@ -0,0 +1,279 @@
import Song from '../../models/Song';
import React from 'react';
import { Image, View } from 'react-native';
import { Pressable, Text, PresenceTransition, Icon } 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>
<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',
}}
>
<Pressable
onHoverIn={() => {
setIsPlayHovered(true);
}}
onHoverOut={() => {
setIsPlayHovered(false);
}}
borderRadius={100}
marginBottom={35}
onPress={props.onPlay}
>
{({ isPressed, isHovered }) => (
<View
style={{
width: 40,
height: 40,
borderRadius: 100,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: (() => {
if (isPressed) {
return 'rgba(96, 117, 249, 1)';
} else if (isHovered) {
return 'rgba(96, 117, 249, 0.9)';
} else {
return 'rgba(96, 117, 249, 0.7)';
}
})(),
}}
>
<Ionicons
name="play-outline"
color={'white'}
size={20}
rounded="sm"
/>
</View>
)}
</Pressable>
</View>
</PresenceTransition>
</>
</Pressable>
</View>
);
};
SongCardInfo.defaultProps = {
onPress: () => {},
onPlay: () => {},
};
export default SongCardInfo;
+142
View File
@@ -0,0 +1,142 @@
import { useBreakpointValue } from 'native-base';
import { View } from 'react-native';
import TabNavigationDesktop from './TabNavigationDesktop';
import TabNavigationPhone from './TabNavigationPhone';
import { Ionicons } from '@expo/vector-icons';
import React, { useState } from 'react';
import useColorScheme from '../../hooks/colorScheme';
import HomeView from '../../views/V2/HomeView';
export type NaviTab = {
id: string;
label: string;
icon?: React.ReactNode;
onPress?: () => void;
onLongPress?: () => void;
isActive?: boolean;
isCollapsed?: boolean;
iconName?: string;
};
const tabs = [
{
id: 'home',
label: 'Discovery',
icon: <Ionicons name="search" size={24} color="black" />,
iconName: 'search',
},
{
id: 'profile',
label: 'Profile',
icon: <Ionicons name="person" size={24} color="black" />,
iconName: 'person',
},
{
id: 'music',
label: 'Music',
icon: <Ionicons name="musical-notes" size={24} color="black" />,
iconName: 'musical-notes',
},
{
id: 'search',
label: 'Search',
icon: <Ionicons name="search" size={24} color="black" />,
iconName: 'search',
},
{
id: 'notifications',
label: 'Notifications',
icon: <Ionicons name="notifications" size={24} color="black" />,
iconName: 'notifications',
},
{
id: 'settings',
label: 'Settings',
icon: <Ionicons name="settings" size={24} color="white" />,
iconName: 'settings',
},
] as NaviTab[];
const TabNavigation = () => {
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
const [isDesktopCollapsed, setIsDesktopCollapsed] = useState(false);
const [activeTab, setActiveTab] = useState(tabs[0]?.id ?? 'home');
const colorScheme = useColorScheme();
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),
icon: (
<Ionicons
// eslint-disable-next-line @typescript-eslint/no-explicit-any
name={t.iconName as any}
size={24}
color={colorScheme === 'dark' ? 'white' : 'black'}
/>
),
};
});
return (
<View
style={{
width: '100%',
height: '100%',
backgroundColor: 'rgb(26, 36, 74)',
}}
>
{screenSize === 'small' ? (
<TabNavigationPhone
tabs={appTabs}
activeTabID={activeTab}
setActiveTabID={setActiveTab}
>
<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
tabs={appTabs}
activeTabID={activeTab}
setActiveTabID={setActiveTab}
isCollapsed={isDesktopCollapsed}
setIsCollapsed={setIsDesktopCollapsed}
>
<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>
);
};
export default TabNavigation;
@@ -0,0 +1,86 @@
import { View } from 'react-native';
import { Pressable, Text } from 'native-base';
import React from 'react';
type TabNavigationButtonProps = {
icon?: React.ReactNode;
label: string;
onPress: () => void;
onLongPress: () => void;
isActive: boolean;
isCollapsed: boolean;
};
const TabNavigationButton = (props: TabNavigationButtonProps) => {
return (
<Pressable
onPress={props.onPress}
onLongPress={props.onLongPress}
style={{
width: '100%',
}}
>
{({ isPressed, isHovered }) => (
<View
style={{
display: 'flex',
flexDirection: 'row',
alignSelf: 'stretch',
alignItems: 'center',
justifyContent: 'flex-start',
padding: '10px',
borderRadius: 8,
flexGrow: 0,
// @ts-expect-error BoxShadow is not in the types but I want it this may be a legitimate error on my part
boxShadow: (() => {
if (isHovered) {
return '0px 0px 16px 0px rgba(0, 0, 0, 0.25)';
} else if (props.isActive) {
return '0px 0px 8px 0px rgba(0, 0, 0, 0.25)';
} else {
return undefined;
}
})(),
backdropFilter: 'blur(2px)',
backgroundColor: (() => {
if (isPressed) {
return 'rgba(0, 0, 0, 0.1)';
} else if (isHovered) {
return 'rgba(231, 231, 232, 0.2)';
} else if (props.isActive) {
return 'rgba(16, 16, 20, 0.5)';
} else {
return 'transparent';
}
})(),
}}
>
{props.icon && (
<View
style={{
marginRight: props.isCollapsed ? undefined : '10px',
}}
>
{props.icon}
</View>
)}
{!props.isCollapsed && (
<Text numberOfLines={1} selectable={false}>
{props.label}
</Text>
)}
</View>
)}
</Pressable>
);
};
TabNavigationButton.defaultProps = {
icon: undefined,
onPress: () => {},
onLongPress: () => {},
isActive: false,
isCollapsed: false,
};
export default TabNavigationButton;
@@ -0,0 +1,180 @@
import { View, Image } from 'react-native';
import { Divider, Text, Center, ScrollView } from 'native-base';
import TabNavigationButton from './TabNavigationButton';
import TabNavigationList from './TabNavigationList';
import { useAssets } from 'expo-asset';
import useColorScheme from '../../hooks/colorScheme';
import { useQuery, useQueries } from '../../Queries';
import { NaviTab } from './TabNavigation';
import API from '../../API';
import Song from '../../models/Song';
type TabNavigationDesktopProps = {
tabs: NaviTab[];
isCollapsed: boolean;
setIsCollapsed: (isCollapsed: boolean) => void;
activeTabID: string;
setActiveTabID: (id: string) => void;
children?: React.ReactNode;
};
const TabNavigationDesktop = (props: TabNavigationDesktopProps) => {
const colorScheme = useColorScheme();
const [icon] = useAssets(
colorScheme == 'light'
? require('../../assets/icon_light.png')
: require('../../assets/icon_dark.png')
);
const playHistoryQuery = useQuery(API.getUserPlayHistory);
const songHistory = useQueries(
playHistoryQuery.data?.map(({ songID }) => API.getSong(songID)) ?? []
);
// settings is displayed separately (with logout)
const buttons = props.tabs.filter((tab) => tab.id !== 'settings');
return (
<View
style={{
display: 'flex',
flexDirection: 'row',
width: '100%',
height: '100%',
}}
>
<View>
<Center>
<View
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-start',
flexShrink: 0,
padding: '10px',
}}
>
<Image
source={{ uri: icon?.at(0)?.uri }}
style={{
aspectRatio: 1,
width: '40px',
height: 'auto',
marginRight: '10px',
}}
/>
<Text fontSize={'2xl'} selectable={false}>
Chromacase
</Text>
</View>
</Center>
<View
style={{
display: 'flex',
width: '300px',
height: 'auto',
padding: '32px',
flexDirection: 'column',
justifyContent: 'space-between',
alignItems: 'flex-start',
flexGrow: 1,
}}
>
<TabNavigationList
style={{
flexShrink: 0,
// @ts-expect-error gap is not in the types because we have an old version of react-native
gap: '20px',
}}
>
{buttons.map((button, index) => (
<TabNavigationButton
key={'tab-navigation-button-' + index}
icon={button.icon}
label={button.label}
isActive={button.id == props.activeTabID}
onPress={button.onPress}
onLongPress={button.onLongPress}
isCollapsed={props.isCollapsed}
/>
))}
</TabNavigationList>
<TabNavigationList>
<Divider />
<TabNavigationList>
<Text
bold
style={{
paddingHorizontal: '16px',
paddingVertical: '10px',
fontSize: 20,
}}
>
Recently played
</Text>
{songHistory.length === 0 && (
<Text
style={{
paddingHorizontal: '16px',
paddingVertical: '10px',
}}
>
No songs played yet
</Text>
)}
{songHistory
.map((h) => h.data)
.filter((data): data is Song => data !== undefined)
.filter(
(song, i, array) =>
array.map((s) => s.id).findIndex((id) => id == song.id) == i
)
.slice(0, 4)
.map((histoItem, index) => (
<View
key={'tab-navigation-other-' + index}
style={{
paddingHorizontal: '16px',
paddingVertical: '10px',
}}
>
<Text numberOfLines={1}>{histoItem.name}</Text>
</View>
))}
</TabNavigationList>
<Divider />
<TabNavigationList
style={{
// @ts-expect-error gap is not in the types because we have an old version of react-native
gap: '20px',
}}
>
{([props.tabs.find((t) => t.id === 'settings')] as NaviTab[]).map(
(button, index) => (
<TabNavigationButton
key={'tab-navigation-setting-button-' + index}
icon={button.icon}
label={button.label}
isActive={button.id == props.activeTabID}
onPress={button.onPress}
onLongPress={button.onLongPress}
isCollapsed={props.isCollapsed}
/>
)
)}
</TabNavigationList>
</TabNavigationList>
</View>
</View>
<ScrollView
style={{
height: '100%',
width: 'calc(100% - 300px)',
}}
>
{props.children}
</ScrollView>
</View>
);
};
export default TabNavigationDesktop;
+29
View File
@@ -0,0 +1,29 @@
import React from 'react';
import { View, StyleProp, ViewStyle } from 'react-native';
type TabNavigationListProps = {
children: React.ReactNode;
style?: StyleProp<ViewStyle>;
};
const TabNavigationList = (props: TabNavigationListProps) => {
return (
<View
style={[
{
display: 'flex',
alignItems: 'flex-start',
alignSelf: 'stretch',
flexDirection: 'column',
// @ts-expect-error gap is not in the types because we have an old version of react-native
gap: '8px',
},
props.style,
]}
>
{props.children}
</View>
);
};
export default TabNavigationList;
@@ -0,0 +1,70 @@
import { View } from 'react-native';
import { Center, ScrollView } from 'native-base';
import TabNavigationButton from './TabNavigationButton';
import { NaviTab } from './TabNavigation';
type TabNavigationPhoneProps = {
tabs: NaviTab[];
activeTabID: string;
setActiveTabID: (id: string) => void;
children?: React.ReactNode;
};
const TabNavigationPhone = (props: TabNavigationPhoneProps) => {
return (
<View
style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column-reverse',
}}
>
<View
style={{
padding: '16px',
height: '90px',
width: '100%',
}}
>
<Center>
<View
style={{
display: 'flex',
padding: '8px',
justifyContent: 'space-evenly',
flexDirection: 'row',
alignItems: 'center',
alignSelf: 'stretch',
borderRadius: 8,
backgroundColor: 'rgba(16, 16, 20, 0.5)',
}}
>
{props.tabs.map((tab) => (
<View key={'navigation-button-phone-' + tab.label}>
<TabNavigationButton
icon={tab.icon}
label={tab.label}
onPress={tab.onPress}
onLongPress={tab.onLongPress}
isActive={tab.id === props.activeTabID}
isCollapsed={tab.id != props.activeTabID}
/>
</View>
))}
</View>
</Center>
</View>
<ScrollView
style={{
width: '100%',
height: 'calc(100% - 90px)',
}}
>
{props.children}
</ScrollView>
</View>
);
};
export default TabNavigationPhone;
@@ -41,7 +41,7 @@ const ChangePasswordForm = ({ onSubmit }: ChangePasswordFormProps) => {
isSecret
isRequired
autoComplete="password"
icon={(size, color) => <Lock1 size={size} color={color} variant="Bold" />}
icon={Lock1}
placeholder={translate('oldPassword')}
value={formData.oldPassword.value}
error={formData.oldPassword.error}
@@ -60,7 +60,7 @@ const ChangePasswordForm = ({ onSubmit }: ChangePasswordFormProps) => {
isSecret
isRequired
autoComplete="password"
icon={(size, color) => <Lock1 size={size} color={color} variant="Bold" />}
icon={Lock1}
placeholder={translate('newPassword')}
value={formData.newPassword.value}
error={formData.newPassword.error}
@@ -79,7 +79,7 @@ const ChangePasswordForm = ({ onSubmit }: ChangePasswordFormProps) => {
isSecret
isRequired
autoComplete="password"
icon={(size, color) => <Lock1 size={size} color={color} variant="Bold" />}
icon={Lock1}
placeholder={translate('confirmNewPassword')}
value={formData.confirmNewPassword.value}
error={formData.confirmNewPassword.error}
+9 -2
View File
@@ -6,9 +6,15 @@ import API from '../API';
export const UserValidator = yup
.object({
username: yup.string().required(),
password: yup.string().required().nullable(),
password: yup
.string()
.nullable()
.transform((value) => (value === '' ? null : value)),
emailVerified: yup.boolean().required(),
email: yup.string().required().nullable(),
email: yup
.string()
.nullable()
.transform((value) => (value === '' ? null : value)),
googleID: yup.string().required().nullable(),
isGuest: yup.boolean().required(),
partyPlayed: yup.number().required(),
@@ -19,6 +25,7 @@ export const UserHandler: ResponseHandler<yup.InferType<typeof UserValidator>, U
validator: UserValidator,
transformer: (value) => ({
...value,
email: value.email ?? null,
name: value.username,
premium: false,
data: {
+29
View File
@@ -0,0 +1,29 @@
server {
listen ${PORT};
root /usr/share/nginx/html;
index index.html;
location /assets {
alias /assets;
}
location / {
proxy_pass ${FRONT_URL}/;
}
location /api/ {
proxy_pass ${API_URL}/;
}
location /ws {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass ${SCOROMETER_URL};
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_http_version 1.1;
}
}
+1
View File
@@ -42,6 +42,7 @@
"expo-secure-store": "~12.0.0",
"expo-splash-screen": "~0.17.5",
"expo-status-bar": "~1.4.2",
"file-loader": "^6.2.0",
"format-duration": "^2.0.0",
"i18next": "^21.8.16",
"iconsax-react-native": "^0.0.8",
+2
View File
@@ -0,0 +1,2 @@
declare module '*.jpg';
declare module '*.png';
+6
View File
@@ -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>
+5
View File
@@ -33,6 +33,7 @@ import { MIDIAccess, MIDIMessageEvent, requestMIDIAccess } from '@motiz88/react-
import * as Linking from 'expo-linking';
import url from 'url';
import { PianoCanvasContext, PianoCanvasMsg, NoteTiming } from '../models/PianoGame';
import { Metronome } from '../components/Metronome';
type PlayViewProps = {
songId: number;
@@ -83,6 +84,7 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
const toast = useToast();
const [lastScoreMessage, setLastScoreMessage] = useState<ScoreMessage>();
const webSocket = useRef<WebSocket>();
const bpm = useRef<number>(60);
const [paused, setPause] = useState<boolean>(true);
const stopwatch = useStopwatch();
const [time, setTime] = useState(0);
@@ -348,6 +350,7 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
>
<PartitionCoord
file={musixml.data}
bpmRef={bpm}
onEndReached={onEnd}
onPause={onPause}
onResume={onResume}
@@ -357,6 +360,8 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
{!partitionRendered && <LoadingComponent />}
</View>
<Metronome paused={paused} bpm={bpm.current} />
<Box
shadow={4}
style={{
+2 -2
View File
@@ -90,7 +90,7 @@ const StartPageView = () => {
image={loginBanner?.at(0)?.uri}
iconName="user"
iconProvider={FontAwesome5}
onPress={() => navigation.navigate('Login', {})}
onPress={() => navigation.navigate('Login')}
style={{
width: isSmallScreen ? '90%' : 'clamp(100px, 33.3%, 600px)',
height: '300px',
@@ -130,7 +130,7 @@ const StartPageView = () => {
subtitle="Create an account to save your progress"
iconProvider={FontAwesome5}
iconName="user-plus"
onPress={() => navigation.navigate('Signup', {})}
onPress={() => navigation.navigate('Signup')}
style={{
height: '150px',
width: isSmallScreen ? '90%' : 'clamp(150px, 50%, 600px)',
+211
View File
@@ -0,0 +1,211 @@
import { View } from 'react-native';
import { Text, useBreakpointValue } from 'native-base';
import React from 'react';
import { useQuery, useQueries } from '../../Queries';
import HomeMainSongCard from '../../components/V2/HomeMainSongCard';
import SongCardInfo from '../../components/V2/SongCardInfo';
import API from '../../API';
import { useNavigation } from '../../Navigation';
const bigSideRatio = 1000;
const smallSideRatio = 618;
type HomeCardProps = {
image: string;
title: string;
artist: string;
fontSize: number;
onPress?: () => void;
};
const cards = [
{
image: 'https://media.discordapp.net/attachments/717080637038788731/1153688155292180560/image_homeview1.png',
title: 'Beethoven',
artist: 'Synphony No. 9',
fontSize: 46,
},
{
image: 'https://media.discordapp.net/attachments/717080637038788731/1153688154923090093/image_homeview2.png',
title: 'Mozart',
artist: 'Lieder Kantate KV 619',
fontSize: 36,
},
{
image: 'https://media.discordapp.net/attachments/717080637038788731/1153688154499457096/image_homeview3.png',
title: 'Back',
artist: 'Truc Truc',
fontSize: 26,
},
{
image: 'https://media.discordapp.net/attachments/717080637038788731/1153688154109394985/image_homeview4.png',
title: 'Mozart',
artist: 'Machin Machin',
fontSize: 22,
},
] as [HomeCardProps, HomeCardProps, HomeCardProps, HomeCardProps];
const HomeView = () => {
const songsQuery = useQuery(API.getSongSuggestions);
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
const isPhone = screenSize === 'small';
const navigation = useNavigation();
const artistsQueries = useQueries(
(songsQuery.data ?? []).map((song) => API.getArtist(song.artistId))
);
React.useEffect(() => {
if (!songsQuery.data) return;
if (artistsQueries.every((query) => !query.isLoading)) return;
(songsQuery.data ?? [])
.filter((song) =>
artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId)
)
.forEach((song, index) => {
if (index > 3) return;
cards[index]!.image = song.cover;
cards[index]!.title = song.name;
cards[index]!.artist = artistsQueries.find(
(artistQuery) => artistQuery.data?.id === song.artistId
)!.data!.name;
cards[index]!.onPress = () => {
navigation.navigate('Song', { songId: song.id });
};
});
}, [artistsQueries]);
return (
<View
style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
}}
>
<View>
<View
style={{
alignSelf: 'stretch',
maxWidth: '1100px',
alignItems: 'stretch',
flexDirection: isPhone ? 'column' : 'row',
}}
>
<View
style={{
flexGrow: bigSideRatio,
}}
>
<HomeMainSongCard {...cards[0]} />
</View>
<View
style={{
flexGrow: smallSideRatio,
display: 'flex',
flexDirection: isPhone ? 'row' : 'column',
alignItems: 'stretch',
}}
>
<View
style={{
flexGrow: bigSideRatio,
}}
>
<HomeMainSongCard {...cards[1]} />
</View>
<View
style={{
flexGrow: smallSideRatio,
display: 'flex',
flexDirection: isPhone ? 'column-reverse' : 'row-reverse',
alignItems: 'stretch',
}}
>
<View
style={{
flexGrow: bigSideRatio,
}}
>
<HomeMainSongCard {...cards[2]} />
</View>
<View
style={{
flexGrow: smallSideRatio,
display: 'flex',
flexDirection: isPhone ? 'row-reverse' : 'column-reverse',
alignItems: 'stretch',
}}
>
<View
style={{
flexGrow: bigSideRatio,
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
justifyContent: 'flex-end',
}}
>
<HomeMainSongCard {...cards[3]} />
</View>
<View
style={{
flexGrow: smallSideRatio,
}}
></View>
</View>
</View>
</View>
</View>
</View>
<View
style={{
flexShrink: 0,
flexGrow: 0,
flexBasis: '15%',
width: '100%',
}}
>
<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={() => {
navigation.navigate('Song', { songId: song.id });
}}
onPlay={() => {
console.log('play');
}}
/>
))}
</View>
</View>
</View>
);
};
export default HomeView;
+25 -1
View File
@@ -6,7 +6,7 @@ import ElementList from '../../components/GtkUI/ElementList';
import { translate } from '../../i18n/i18n';
import { useQuery } from '../../Queries';
import * as ImagePicker from 'expo-image-picker';
import { Google, PasswordCheck, SmsEdit, UserSquare } from 'iconsax-react-native';
import { Google, PasswordCheck, SmsEdit, UserSquare, Verify } from 'iconsax-react-native';
import ChangeEmailForm from '../../components/forms/changeEmailForm';
import ChangePasswordForm from '../../components/forms/changePasswordForm';
@@ -53,6 +53,30 @@ const ProfileSettings = () => {
text: user.googleID ? 'Linked' : 'Not linked',
},
},
{
icon: <Verify size="24" color="#FFF" style={{ minWidth: 24 }} />,
type: 'text',
description: 'Vérifiez votre adresse e-mail', // TODO translate
title: translate('verified'),
data: {
text: user.emailVerified ? 'verified' : 'not verified',
onPress: user.emailVerified
? undefined
: () =>
API.fetch({ route: '/auth/reverify', method: 'PUT' })
.then(() =>
Toast.show({
description: 'Verification mail sent',
})
)
.catch((e) => {
console.error(e);
Toast.show({
description: 'Verification mail send error',
});
}),
},
},
{
icon: <UserSquare size="24" color="#FFF" style={{ minWidth: 24 }} />,
type: 'text',
+2 -1
View File
@@ -5,9 +5,10 @@ pkgs.mkShell {
nodePackages.prisma
nodePackages."@nestjs/cli"
nodePackages.npm
eslint_d
nodejs_16
yarn
python3
(python3.withPackages (ps: with ps; [requests]))
pkg-config
];
shellHook = with pkgs; ''