Redesign profil with datafake for skills

This commit is contained in:
mathysPaul
2023-09-20 17:40:52 +02:00
parent 973f9bf5b3
commit 94a64d16e6
11 changed files with 1376 additions and 458 deletions
+201 -52
View File
@@ -1,7 +1,20 @@
import { Box, useBreakpointValue, useTheme } from 'native-base';
import {
Box,
Column,
Row,
Select,
useBreakpointValue,
useTheme,
Text,
ScrollView,
View,
} from 'native-base';
import { LineChart } from 'react-native-chart-kit';
import SongHistory from '../models/SongHistory';
import { useState } from 'react';
import { useWindowDimensions } from 'react-native';
import CheckboxBase from './UI/CheckboxBase';
import { Dataset } from 'react-native-chart-kit/dist/HelperTypes';
type ScoreGraphProps = {
// The result of the call to API.getSongHistory
@@ -9,69 +22,205 @@ type ScoreGraphProps = {
};
const formatScoreDate = (playDate: Date): string => {
const pad = (n: number) => n.toString().padStart(2, '0');
const formattedDate = `${pad(playDate.getDay())}/${pad(playDate.getMonth())}`;
const formattedTime = `${pad(playDate.getHours())}:${pad(playDate.getMinutes())}`;
return `${formattedDate} ${formattedTime}`;
// const formattedDate = `${pad(playDate.getDay())}/${pad(playDate.getMonth())}`;
// const formattedTime = `${pad(playDate.getHours())}:${pad(playDate.getMinutes())}`;
return `${playDate.getDate()}/${playDate.getMonth()}`;
};
const ScoreGraph = (props: ScoreGraphProps) => {
const layout = useWindowDimensions();
const [selectedRange, setSelectedRange] = useState('3days');
const [displayScore, setDisplayScore] = useState(true);
const [displayPedals, setDisplayPedals] = useState(false);
const [displayRightHand, setDisplayRightHand] = useState(false);
const [displayLeftHand, setDisplayLeftHand] = useState(false);
const [displayAccuracy, setDisplayAccuracy] = useState(false);
const [displayArpeges, setDisplayArpeges] = useState(false);
const [displayChords, setDisplayChords] = useState(false);
const rangeOptions = [
{ label: '3 derniers jours', value: '3days' },
{ label: 'Dernière semaine', value: 'week' },
{ label: 'Dernier mois', value: 'month' },
];
const scores = props.songHistory.history.sort((a, b) => {
if (a.playDate < b.playDate) {
return -1;
} else if (a.playDate > b.playDate) {
return 1;
}
return 0;
});
const filterData = () => {
const oneWeekAgo = new Date();
const oneMonthAgo = new Date();
const threeDaysAgo = new Date();
switch (selectedRange) {
case 'week':
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
return scores.filter((item) => item.playDate >= oneWeekAgo);
case 'month':
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
return scores.filter((item) => item.playDate > oneMonthAgo);
default:
threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
return scores.filter((item) => item.playDate >= threeDaysAgo);
}
};
const theme = useTheme();
const [containerWidth, setContainerWidth] = useState(0);
// We sort the scores by date, asc.
// By default, the API returns them in desc.
// const pointsToDisplay = props.width / 100;
const isSmall = useBreakpointValue({ base: true, md: false });
const scores = props.songHistory.history
.sort((a, b) => {
if (a.playDate < b.playDate) {
return -1;
} else if (a.playDate > b.playDate) {
return 1;
}
return 0;
})
.slice(-10);
const tempDatasets: Dataset[] = [];
const skills = [
{
title: 'Score',
value: 'score',
data: filterData().map(({ score }) => score),
color: '#5f74f7',
check: displayScore,
setCheck: setDisplayScore,
},
{
title: 'Pedals',
value: 'pedals',
color: '#ae84fb',
data: filterData().map(({ score }) => (score > 100 ? score - 100 : score * 1.4)),
check: displayPedals,
setCheck: setDisplayPedals,
},
{
title: 'Right hand',
value: 'rightHand',
data: filterData().map(({ score }) => (score > 10 ? score - 10 : score * 0.2)),
color: '#a61455',
check: displayRightHand,
setCheck: setDisplayRightHand,
},
{
title: 'Left hand',
value: 'leftHand',
data: filterData().map(({ score }) => (score > 50 ? score - 50 : score * 0.8)),
color: '#ed4a51',
check: displayLeftHand,
setCheck: setDisplayLeftHand,
},
{
title: 'Accuracy',
value: 'accuracy',
data: filterData().map(({ score }) => (score > 40 ? score - 40 : score * 0.4)),
color: '#ff7a72',
check: displayAccuracy,
setCheck: setDisplayAccuracy,
},
{
title: 'Arpeges',
value: 'arpeges',
data: filterData().map(({ score }) => (score > 200 ? score - 200 : score * 1.2)),
color: '#ead93c',
check: displayArpeges,
setCheck: setDisplayArpeges,
},
{
title: 'Chords',
value: 'chords',
data: filterData().map(({ score }) => (score > 50 ? score - 50 : score)),
color: '#73d697',
check: displayChords,
setCheck: setDisplayChords,
},
];
for (const skill of skills) {
if (skill.check) {
tempDatasets.push({
data: skill.data,
color: () => skill.color,
});
}
}
return (
<Box
style={{ width: '100%' }}
onLayout={(event) => setContainerWidth(event.nativeEvent.layout.width)}
>
<LineChart
data={{
labels: isSmall ? [] : scores.map(({ playDate }) => formatScoreDate(playDate)),
datasets: [
{
data: scores.map(({ score }) => score),
},
],
}
}
width={containerWidth}
height={200} // Completely arbitrary
transparent={true}
withDots={false}
yAxisSuffix=" pts"
chartConfig={{
decimalPlaces: 0,
color: (opacity = 1) => `rgba(255, 255, 255, ${opacity})`,
labelColor: () => theme.colors.white,
propsForDots: {
r: '6',
strokeWidth: '2',
},
<Column>
<Row
style={{
alignItems: 'center',
}}
bezier
// style={{
// margin: 3,
// shadowColor: theme.colors.primary[400],
// shadowOpacity: 1,
// shadowRadius: 20,
// borderRadius: CardBorderRadius,
// }}
/>
</Box>
>
<Text>Skils</Text>
<ScrollView horizontal={true}>
{skills.map((skill) => (
<View key={skill.value} style={{ paddingLeft: 20 }}>
<CheckboxBase
title={skill.title}
value={skill.value}
check={skill.check}
setCheck={skill.setCheck}
/>
</View>
))}
</ScrollView>
<Select
selectedValue={selectedRange}
onValueChange={(itemValue) => setSelectedRange(itemValue)}
defaultValue={'3days'}
bgColor={'rgba(16,16,20,0.5)'}
variant="filled"
style={{ display: 'flex', justifyContent: 'center' }}
width={layout.width > 650 ? '200' : '100'}
>
{rangeOptions.map((option) => (
<Select.Item key={option.label} label={option.label} value={option.value} />
))}
</Select>
</Row>
<Box
style={{ width: '100%', marginTop: 20 }}
onLayout={(event) => setContainerWidth(event.nativeEvent.layout.width)}
>
{tempDatasets.length > 0 && (
<LineChart
data={{
labels: isSmall
? []
: filterData().map(({ playDate }) => formatScoreDate(playDate)),
datasets: tempDatasets,
}}
width={containerWidth}
height={300} // Completely arbitrary
transparent={true}
yAxisSuffix=" pts"
chartConfig={{
propsForLabels: {
fontFamily: 'Lexend',
},
propsForVerticalLabels: {
rotation: -90,
},
propsForBackgroundLines: {
strokeDasharray: '',
strokeWidth: '1',
color: '#fff000',
},
decimalPlaces: 0,
color: (opacity = 1) => `rgba(255, 255, 255, ${opacity})`,
labelColor: () => theme.colors.white,
propsForDots: {
r: '6',
strokeWidth: '2',
},
}}
bezier
/>
)}
</Box>
</Column>
);
};
+4 -1
View File
@@ -107,12 +107,15 @@ const ButtonBase: React.FC<ButtonProps> = ({
>
{loading ? (
<ActivityIndicator
style={styles.content}
size="small"
color={type === 'outlined' ? '#6075F9' : '#FFFFFF'}
/>
) : (
<View style={styles.content}>
{icon && <MyIcon size={'18'} color={type === 'outlined' ? '#6075F9' : '#FFFFFF'}/>}
{icon && (
<MyIcon size={'18'} color={type === 'outlined' ? '#6075F9' : '#FFFFFF'} />
)}
{iconImage && <Image source={{ uri: iconImage }} style={styles.icon} />}
{title && <Text style={styles.text}>{title}</Text>}
</View>
+82
View File
@@ -0,0 +1,82 @@
import React from 'react';
import { StyleSheet, View, StyleProp, ViewStyle } from 'react-native';
import InteractiveBase from './InteractiveBase';
import { Checkbox } from 'native-base';
interface CheckboxProps {
title: string;
value: string;
// color: string;
check: boolean;
setCheck: (value: boolean) => void;
style?: StyleProp<ViewStyle>;
}
const CheckboxBase: React.FC<CheckboxProps> = ({
title,
value,
// color,
style,
check,
setCheck,
}) => {
const styleGlassmorphism = StyleSheet.create({
Default: {
scale: 1,
shadowOpacity: 0.3,
shadowRadius: 4.65,
elevation: 8,
backgroundColor: 'rgba(16,16,20,0.5)',
},
onHover: {
scale: 1.01,
shadowOpacity: 0.37,
shadowRadius: 7.49,
elevation: 12,
backgroundColor: 'rgba(16,16,20,0.4)',
},
onPressed: {
scale: 0.99,
shadowOpacity: 0.23,
shadowRadius: 2.62,
elevation: 4,
backgroundColor: 'rgba(16,16,20,0.6)',
},
Disabled: {
scale: 1,
shadowOpacity: 0.3,
shadowRadius: 4.65,
elevation: 8,
backgroundColor: 'rgba(16,16,20,0.5)',
},
});
return (
<InteractiveBase
style={[styles.container, style]}
styleAnimate={styleGlassmorphism}
onPress={async () => {
setCheck(!check);
}}
>
<View style={{ paddingVertical: 5, paddingHorizontal: 10 }}>
<Checkbox isChecked={check} style={styles.content} value={value}>
{title}
</Checkbox>
</View>
</InteractiveBase>
);
};
const styles = StyleSheet.create({
container: {
borderRadius: 8,
},
content: {
justifyContent: 'center',
flexDirection: 'row',
alignItems: 'center',
},
});
export default CheckboxBase;
+1 -1
View File
@@ -233,7 +233,7 @@ const InteractiveBase: React.FC<InteractiveBaseProps> = ({
shadowOpacity: styleAnimate.Disabled.shadowOpacity,
shadowRadius: styleAnimate.Disabled.shadowRadius,
elevation: styleAnimate.Disabled.elevation,
}
};
return (
<Animated.View style={[style, isDisabled ? disableStyle : animatedStyle]}>
+102 -63
View File
@@ -1,73 +1,112 @@
import { LinearGradient } from "expo-linear-gradient";
import { Center, Flex, Stack, View, Text, Wrap, Image } from "native-base";
import { FunctionComponent } from "react";
import { Linking, useWindowDimensions } from "react-native";
import ButtonBase from "./ButtonBase";
import { translate } from "../../i18n/i18n";
import API from "../../API";
import SeparatorBase from "./SeparatorBase";
import LinkBase from "./LinkBase";
import { LinearGradient } from 'expo-linear-gradient';
import { Center, Flex, Stack, View, Text, Wrap, Image } from 'native-base';
import { FunctionComponent } from 'react';
import { Linking, useWindowDimensions } from 'react-native';
import ButtonBase from './ButtonBase';
import { translate } from '../../i18n/i18n';
import API from '../../API';
import SeparatorBase from './SeparatorBase';
import LinkBase from './LinkBase';
import ImageBanner from '../../assets/banner.jpg';
interface ScaffoldAuthProps {
title: string;
description: string;
form: React.ReactNode[];
submitButton: React.ReactNode;
link: {text: string, description: string, onPress: () => void};
title: string;
description: string;
form: React.ReactNode[];
submitButton: React.ReactNode;
link: { text: string; description: string; onPress: () => void };
}
const ScaffoldAuth: FunctionComponent<ScaffoldAuthProps> = ({title, description, form, submitButton, link}) => {
const layout = useWindowDimensions();
const ScaffoldAuth: FunctionComponent<ScaffoldAuthProps> = ({
title,
description,
form,
submitButton,
link,
}) => {
const layout = useWindowDimensions();
return (
<Flex direction='row' justifyContent="space-between" style={{ flex: 1, backgroundColor: '#101014'}}>
<Center style={{ flex: 1}}>
<View style={{ width: '100%', maxWidth: 420, padding: 16 }}>
<Stack space={8} justifyContent="center" alignContent="center" alignItems="center" style={{ width: '100%', paddingBottom: 40}}>
<Text fontSize="4xl" textAlign="center">{title}</Text>
<Text fontSize="lg" textAlign="center">{description}</Text>
</Stack>
<Stack space={5} justifyContent="center" alignContent="center" alignItems="center" style={{ width: '100%'}}>
<ButtonBase
style={{width: '100%'}}
type='outlined'
iconImage='https://upload.wikimedia.org/wikipedia/commons/thumb/5/53/Google_%22G%22_Logo.svg/2008px-Google_%22G%22_Logo.svg.png'
title={translate('continuewithgoogle')}
onPress={() => Linking.openURL(`${API.baseUrl}/auth/login/google`)}
/>
<SeparatorBase>or</SeparatorBase>
<Stack space={3} justifyContent="center" alignContent="center" alignItems="center" style={{ width: '100%'}}>
{form}
</Stack>
{submitButton}
<Wrap style={{flexDirection: 'row', justifyContent: 'center'}}>
<Text>{link.description}</Text>
<LinkBase onPress={link.onPress}>
{link.text}
</LinkBase>
</Wrap>
</Stack>
</View>
</Center>
{
layout.width > 650 ?
<View style={{width: '50%', height: '100%', padding: 16}}>
<Image
source={ImageBanner}
alt="banner page"
style={{width: '100%', height: '100%', borderRadius: 8}}
/>
</View>
: <></>
}
<LinearGradient
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
colors={['#101014', '#6075F9']}
style={{top: 0, bottom: 0, right: 0, left: 0, width: '100%', height: '100%', position: 'absolute', zIndex: -2}}
/>
</Flex>
<Flex
direction="row"
justifyContent="space-between"
style={{ flex: 1, backgroundColor: '#101014' }}
>
<Center style={{ flex: 1 }}>
<View style={{ width: '100%', maxWidth: 420, padding: 16 }}>
<Stack
space={8}
justifyContent="center"
alignContent="center"
alignItems="center"
style={{ width: '100%', paddingBottom: 40 }}
>
<Text fontSize="4xl" textAlign="center">
{title}
</Text>
<Text fontSize="lg" textAlign="center">
{description}
</Text>
</Stack>
<Stack
space={5}
justifyContent="center"
alignContent="center"
alignItems="center"
style={{ width: '100%' }}
>
<ButtonBase
style={{ width: '100%' }}
type="outlined"
iconImage="https://upload.wikimedia.org/wikipedia/commons/thumb/5/53/Google_%22G%22_Logo.svg/2008px-Google_%22G%22_Logo.svg.png"
title={translate('continuewithgoogle')}
onPress={() => Linking.openURL(`${API.baseUrl}/auth/login/google`)}
/>
<SeparatorBase>or</SeparatorBase>
<Stack
space={3}
justifyContent="center"
alignContent="center"
alignItems="center"
style={{ width: '100%' }}
>
{form}
</Stack>
{submitButton}
<Wrap style={{ flexDirection: 'row', justifyContent: 'center' }}>
<Text>{link.description}</Text>
<LinkBase onPress={link.onPress}>{link.text}</LinkBase>
</Wrap>
</Stack>
</View>
</Center>
{layout.width > 650 ? (
<View style={{ width: '50%', height: '100%', padding: 16 }}>
<Image
source={ImageBanner}
alt="banner page"
style={{ width: '100%', height: '100%', borderRadius: 8 }}
/>
</View>
) : (
<></>
)}
<LinearGradient
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
colors={['#101014', '#6075F9']}
style={{
top: 0,
bottom: 0,
right: 0,
left: 0,
width: '100%',
height: '100%',
position: 'absolute',
zIndex: -2,
}}
/>
</Flex>
);
};
+7 -1
View File
@@ -103,7 +103,13 @@ const TextFieldBase: React.FC<TextFieldBaseProps> = ({
<InteractiveBase style={[style, { borderRadius: 12 }]} styleAnimate={styleAnimate}>
<View style={styles.container}>
<View style={styles.iconContainerLeft}>
{icon && <MyIcon size={'20'} color={iconColor ? iconColor : isFocused ? '#5f74f7' : '#394694'} variant="Bold"/>}
{icon && (
<MyIcon
size={'20'}
color={iconColor ? iconColor : isFocused ? '#5f74f7' : '#394694'}
variant="Bold"
/>
)}
</View>
<Input
variant="unstyled"
+3 -1
View File
@@ -12,7 +12,7 @@ const getInitials = (name: string) => {
type UserAvatarProps = Pick<Parameters<typeof Avatar>[0], 'size'>;
const UserAvatar = ({ size }: UserAvatarProps) => {
const UserAvatar = ({ size = 'md' }: UserAvatarProps) => {
const user = useQuery(API.getUserInfo);
const avatarUrl = useMemo(() => {
if (!user.data) {
@@ -25,7 +25,9 @@ const UserAvatar = ({ size }: UserAvatarProps) => {
return (
<Avatar
borderRadius={12}
size={size}
_image={{ borderRadius: 12 }}
source={avatarUrl ? { uri: avatarUrl.toString() } : undefined}
style={{ zIndex: 0 }}
>
+2 -2
View File
@@ -33,7 +33,7 @@ const ChangeEmailForm = ({ onSubmit }: ChangeEmailFormProps) => {
<TextFormField
style={{ marginVertical: 10 }}
isRequired
icon={(size, color) => <Sms size={size} color={color} variant="Bold" />}
icon={Sms}
placeholder={translate('oldEmail')}
value={formData.oldEmail.value}
error={formData.oldEmail.error}
@@ -51,7 +51,7 @@ const ChangeEmailForm = ({ onSubmit }: ChangeEmailFormProps) => {
style={{ marginVertical: 10 }}
isRequired
autoComplete="off"
icon={(size, color) => <Sms size={size} color={color} variant="Bold" />}
icon={Sms}
placeholder={translate('newEmail')}
value={formData.newEmail.value}
error={formData.newEmail.error}