diff --git a/front/API.ts b/front/API.ts index c8555e4..876f874 100644 --- a/front/API.ts +++ b/front/API.ts @@ -68,7 +68,7 @@ export default class API { public static readonly baseUrl = process.env.NODE_ENV != 'development' && Platform.OS === 'web' ? '/api' - : Constants.manifest?.extra?.apiUrl; + : "https://nightly.chroma.octohub.app/api"; public static async fetch( params: FetchParams, handle: Pick, 'raw'> diff --git a/front/App.tsx b/front/App.tsx index 243702a..4b017cf 100644 --- a/front/App.tsx +++ b/front/App.tsx @@ -10,6 +10,7 @@ import LanguageGate from './i18n/LanguageGate'; import ThemeProvider, { ColorSchemeProvider } from './Theme'; import 'react-native-url-polyfill/auto'; import { QueryRules } from './Queries'; +import { useFonts } from 'expo-font'; const queryClient = new QueryClient(QueryRules); @@ -17,6 +18,10 @@ export default function App() { SplashScreen.preventAutoHideAsync(); setTimeout(SplashScreen.hideAsync, 500); + const [fontsLoaded] = useFonts({ + 'Lexend': require('./assets/fonts/Lexend-VariableFont_wght.ttf'), + }); + return ( diff --git a/front/Theme.tsx b/front/Theme.tsx index 6d7fc57..704282a 100644 --- a/front/Theme.tsx +++ b/front/Theme.tsx @@ -12,18 +12,23 @@ const ThemeProvider = ({ children }: { children: JSX.Element }) => { useSystemColorMode: false, initialColorMode: colorScheme, }, + fonts: { + heading: "Lexend", + body: "Lexend", + mono: "Lexend", + }, colors: { primary: { - 50: '#e6faea', - 100: '#c8e7d0', - 200: '#a7d6b5', - 300: '#86c498', - 400: '#65b47c', - 500: '#4b9a62', - 600: '#3a784b', - 700: '#275635', - 800: '#14341f', - 900: '#001405', + 50: '#eff1fe', + 100: '#e7eafe', + 200: '#cdd4fd', + 300: '#5f74f7', + 400: '#5668de', + 500: '#4c5dc6', + 600: '#4757b9', + 700: '#394694', + 800: '#2b346f', + 900: '#212956', }, secondary: { 50: '#d8ffff', diff --git a/front/assets/fonts/Lexend-VariableFont_wght.ttf b/front/assets/fonts/Lexend-VariableFont_wght.ttf new file mode 100644 index 0000000..b294dc8 Binary files /dev/null and b/front/assets/fonts/Lexend-VariableFont_wght.ttf differ diff --git a/front/components/GtkUI/Element.tsx b/front/components/GtkUI/Element.tsx index 931c2e6..d3dd14f 100644 --- a/front/components/GtkUI/Element.tsx +++ b/front/components/GtkUI/Element.tsx @@ -1,11 +1,15 @@ -import React from 'react'; +import React, { useRef, useState } from 'react'; import { ElementProps } from './ElementTypes'; import { RawElement } from './RawElement'; -import { Pressable, IPressableProps } from 'native-base'; +import { Pressable, IPressableProps, View, Text, Wrap, Column } from 'native-base'; +import { Animated, StyleSheet } from 'react-native'; +import InteractiveBase from '../UI/InteractiveBase'; export const Element = (props: T) => { - let actionFunction: IPressableProps['onPress'] = null; - + let actionFunction: (() => void) | null | undefined = null; + const dropdownAnimator = useRef(new Animated.Value(1)).current; + const [dropdownValue, setDropdownValue] = useState(false); + switch (props.type) { case 'text': actionFunction = props.data?.onPress; @@ -13,18 +17,77 @@ export const Element = (props: T) => { case 'toggle': actionFunction = props.data?.onToggle; break; + case 'sectionDropdown': + actionFunction = () => { + props.data.value = !props.data.value; + setDropdownValue(!dropdownValue); + }; + break; default: break; } + const styleSetting = StyleSheet.create({ + Default: { + scale: 1, + shadowOpacity: 0, + shadowRadius: 0, + elevation: 0, + backgroundColor: 'rgba(16, 16, 20, 0.50)', + }, + onHover: { + scale: 1, + shadowOpacity: 0, + shadowRadius: 0, + elevation: 0, + backgroundColor: 'rgba(32, 32, 40, 0.50)', + }, + onPressed: { + scale: 1, + shadowOpacity: 0, + shadowRadius: 0, + elevation: 0, + backgroundColor: 'rgba(16, 16, 20, 0.50)', + }, + Disabled: { + scale: 1, + shadowOpacity: 0, + shadowRadius: 0, + elevation: 0, + backgroundColor: 'rgba(16, 16, 20, 0.50)', + } + }); + if (!props?.disabled && actionFunction) { return ( - - {({ isHovered }) => { - return ; - }} - + + {actionFunction?.();}} + > + + + { + props.type === 'sectionDropdown' && dropdownValue && + + { + props.data.section.map((value, index) => ( + + {value} + + )) + } + + } + ); } - return ; + return ; }; diff --git a/front/components/GtkUI/ElementList.tsx b/front/components/GtkUI/ElementList.tsx index 42a5657..dcf7e40 100644 --- a/front/components/GtkUI/ElementList.tsx +++ b/front/components/GtkUI/ElementList.tsx @@ -5,6 +5,8 @@ import useColorScheme from '../../hooks/colorScheme'; import { ElementProps } from './ElementTypes'; import { Box, Column, Divider } from 'native-base'; +import InteractiveBase from '../UI/InteractiveBase'; +import { StyleSheet } from 'react-native'; type ElementListProps = { elements: ElementProps[]; @@ -13,12 +15,13 @@ type ElementListProps = { const ElementList = ({ elements, style }: ElementListProps) => { const colorScheme = useColorScheme(); - const isDark = colorScheme === 'dark'; + // const isDark = colorScheme === 'dark'; const elementStyle = { borderRadius: 10, - boxShadow: isDark - ? '0px 0px 3px 0px rgba(255,255,255,0.6)' - : '0px 0px 3px 0px rgba(0,0,0,0.4)', + shadowOpacity: 0.30, + shadowRadius: 4.65, + elevation: 8, + backgroundColor: 'transparent', overflow: 'hidden', } as const; diff --git a/front/components/GtkUI/ElementTypes.tsx b/front/components/GtkUI/ElementTypes.tsx index 278c7be..35eab89 100644 --- a/front/components/GtkUI/ElementTypes.tsx +++ b/front/components/GtkUI/ElementTypes.tsx @@ -1,5 +1,6 @@ -import { Select, Switch, Text, Icon, Row, Slider } from 'native-base'; +import { Select, Switch, Text, Icon, Row, Slider, Box } from 'native-base'; import { MaterialIcons } from '@expo/vector-icons'; +import { useWindowDimensions } from 'react-native'; export type ElementProps = { title: string; @@ -12,6 +13,7 @@ export type ElementProps = { | { type: 'toggle'; data: ElementToggleProps } | { type: 'dropdown'; data: ElementDropdownProps } | { type: 'range'; data: ElementRangeProps } + | { type: 'sectionDropdown'; data: SectionDropdownProps } | { type: 'custom'; data: React.ReactNode } ); @@ -31,6 +33,11 @@ export type ElementToggleProps = { defaultValue?: boolean; }; +export type SectionDropdownProps = { + value: boolean; + section: React.ReactNode[]; +}; + export type ElementDropdownProps = { options: DropdownOption[]; onSelect: (value: string) => void; @@ -93,16 +100,23 @@ export const getElementDropdownNode = ( { options, onSelect, value, defaultValue }: ElementDropdownProps, disabled: boolean ) => { + const layout = useWindowDimensions(); return ( ); @@ -113,6 +127,7 @@ export const getElementRangeNode = ( disabled: boolean, title: string ) => { + const layout = useWindowDimensions(); return ( 650 ? "200" : "100"} > diff --git a/front/components/GtkUI/RawElement.tsx b/front/components/GtkUI/RawElement.tsx index a4608c6..a2e776b 100644 --- a/front/components/GtkUI/RawElement.tsx +++ b/front/components/GtkUI/RawElement.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Box, Button, Column, Icon, Popover, Row, Text, useBreakpointValue } from 'native-base'; +import { Box, Button, Column, Icon, Popover, Row, Text, Wrap, useBreakpointValue } from 'native-base'; import useColorScheme from '../../hooks/colorScheme'; import { Ionicons } from '@expo/vector-icons'; import { ElementProps } from './ElementTypes'; @@ -9,118 +9,127 @@ import { getElementToggleNode, getElementRangeNode, } from './ElementTypes'; +import { ArrowDown2 } from 'iconsax-react-native'; type RawElementProps = { element: ElementProps; - isHovered?: boolean; }; -export const RawElement = ({ element, isHovered }: RawElementProps) => { +export const RawElement = ({ element }: RawElementProps) => { const { title, icon, type, helperText, description, disabled, data } = element; const colorScheme = useColorScheme(); const isDark = colorScheme === 'dark'; const screenSize = useBreakpointValue({ base: 'small', md: 'big' }); const isSmallScreen = screenSize === 'small'; return ( - - - {icon} - - - {title} - - {description && ( - - {description} - - )} - - - - - {helperText && ( - ( - - - - + + ()} + placeholder={translate('oldEmail')} + value={formData.oldEmail.value} + error={formData.oldEmail.error} + onChangeText={(t) => { + let error: null | string = null; + validationSchemas.email + .validate(t) + .catch((e) => (error = e.message)) + .finally(() => { + setFormData({ ...formData, oldEmail: { value: t, error } }); + }); + }} + /> + ()} + placeholder={translate('newEmail')} + value={formData.newEmail.value} + error={formData.newEmail.error} + onChangeText={(t) => { + let error: null | string = null; + validationSchemas.email + .validate(t) + .catch((e) => (error = e.message)) + .finally(() => { + setFormData({ ...formData, newEmail: { value: t, error } }); + }); + }} + /> + { + try { + const resp = await onSubmit( + formData.oldEmail.value, + formData.newEmail.value + ); + toast.show({ description: resp }); + } catch (e) { + toast.show({ description: e as string }); + } + }} + /> + ); }; diff --git a/front/components/forms/changePasswordForm.tsx b/front/components/forms/changePasswordForm.tsx index e7b4c9d..1818256 100644 --- a/front/components/forms/changePasswordForm.tsx +++ b/front/components/forms/changePasswordForm.tsx @@ -1,7 +1,10 @@ import React from 'react'; import { translate } from '../../i18n/i18n'; import { string } from 'yup'; -import { FormControl, Input, Stack, WarningOutlineIcon, Box, Button, useToast } from 'native-base'; +import { FormControl, Input, Stack, WarningOutlineIcon, Box, Button, useToast, Flex } from 'native-base'; +import TextFormField from '../UI/TextFormField'; +import { Lock1 } from 'iconsax-react-native'; +import ButtonBase from '../UI/ButtonBase'; interface ChangePasswordFormProps { onSubmit: (oldPassword: string, newPassword: string) => Promise; @@ -22,7 +25,6 @@ const ChangePasswordForm = ({ onSubmit }: ChangePasswordFormProps) => { error: null as string | null, }, }); - const [submittingForm, setSubmittingForm] = React.useState(false); const validationSchemas = { password: string() @@ -33,111 +35,93 @@ const ChangePasswordForm = ({ onSubmit }: ChangePasswordFormProps) => { const toast = useToast(); return ( - - - + ()} + placeholder={translate('oldPassword')} + value={formData.oldPassword.value} + error={formData.oldPassword.error} + onChangeText={(t) => { + let error: null | string = null; + validationSchemas.password + .validate(t) + .catch((e) => (error = e.message)) + .finally(() => { + setFormData({ ...formData, oldPassword: { value: t, error } }); + }); + }} + /> + ()} + placeholder={translate('newPassword')} + value={formData.newPassword.value} + error={formData.newPassword.error} + onChangeText={(t) => { + let error: null | string = null; + validationSchemas.password + .validate(t) + .catch((e) => (error = e.message)) + .finally(() => { + setFormData({ ...formData, newPassword: { value: t, error } }); + }); + }} + /> + ()} + placeholder={translate('confirmNewPassword')} + value={formData.confirmNewPassword.value} + error={formData.confirmNewPassword.error} + onChangeText={(t) => { + let error: null | string = null; + validationSchemas.password + .validate(t) + .catch((e) => (error = e.message)); + if (!error && t !== formData.newPassword.value) { + error = translate('passwordsDontMatch'); } - > - {translate('oldPassword')} - { - let error: null | string = null; - validationSchemas.password - .validate(t) - .catch((e) => (error = e.message)) - .finally(() => { - setFormData({ ...formData, oldPassword: { value: t, error } }); - }); - }} - /> - }> - {formData.oldPassword.error} - - - {translate('newPassword')} - { - let error: null | string = null; - validationSchemas.password - .validate(t) - .catch((e) => (error = e.message)) - .finally(() => { - setFormData({ ...formData, newPassword: { value: t, error } }); - }); - }} - /> - }> - {formData.newPassword.error} - - - {translate('confirmNewPassword')} - { - let error: null | string = null; - validationSchemas.password - .validate(t) - .catch((e) => (error = e.message)); - if (!error && t !== formData.newPassword.value) { - error = translate('passwordsDontMatch'); - } - setFormData({ - ...formData, - confirmNewPassword: { value: t, error }, - }); - }} - /> - }> - {formData.confirmNewPassword.error} - - - - - - + setFormData({ + ...formData, + confirmNewPassword: { value: t, error }, + }); + }} + /> + { + try { + const resp = await onSubmit( + formData.oldPassword.value, + formData.newPassword.value + ); + toast.show({ description: resp }); + } catch (e) { + toast.show({ description: e as string }); + } + }} + /> + ); }; diff --git a/front/components/navigators/TabRowNavigator.tsx b/front/components/navigators/TabRowNavigator.tsx index 920a92a..aa51885 100644 --- a/front/components/navigators/TabRowNavigator.tsx +++ b/front/components/navigators/TabRowNavigator.tsx @@ -99,13 +99,13 @@ function TabNavigator({ return ( - + {(!isMobileView || isPanelView) && ( { const { settings, updateSettings } = useUserSettings(); @@ -12,10 +13,13 @@ const NotificationsView = () => { return ; } return ( -
- - - + { elements={[ { type: 'toggle', + icon: , title: translate('SettingsNotificationsPushNotifications'), + description: 'Cette notification apparaitra sur votre apparail en pop-up', data: { value: settings.data.notifications.pushNotif, onToggle: () => { @@ -39,7 +45,9 @@ const NotificationsView = () => { }, { type: 'toggle', + icon: , title: translate('SettingsNotificationsEmailNotifications'), + description: 'Recevez des mails pour atteindre vos objectifs', data: { value: settings.data.notifications.emailNotif, onToggle: () => { @@ -53,7 +61,9 @@ const NotificationsView = () => { }, { type: 'toggle', + icon: , title: translate('SettingsNotificationsTrainingReminder'), + description: 'Un apprentissage régulier est la clé', data: { value: settings.data.notifications.trainNotif, onToggle: () => { @@ -67,7 +77,9 @@ const NotificationsView = () => { }, { type: 'toggle', + icon: , title: translate('SettingsNotificationsReleaseAlert'), + description: 'Restez informé de nos mises à jour', data: { value: settings.data.notifications.newSongNotif, onToggle: () => { @@ -81,7 +93,7 @@ const NotificationsView = () => { }, ]} /> -
+ ); }; diff --git a/front/views/settings/PreferencesView.tsx b/front/views/settings/PreferencesView.tsx index 1ed2eb0..e79c400 100644 --- a/front/views/settings/PreferencesView.tsx +++ b/front/views/settings/PreferencesView.tsx @@ -1,22 +1,26 @@ import React from 'react'; import { useDispatch } from 'react-redux'; -import { Center, Heading } from 'native-base'; +import { Center, Flex, Heading } from 'native-base'; import { useLanguage } from '../../state/LanguageSlice'; import { AvailableLanguages, DefaultLanguage, translate, Translate } from '../../i18n/i18n'; import { useSelector } from '../../state/Store'; import { updateSettings } from '../../state/SettingsSlice'; import ElementList from '../../components/GtkUI/ElementList'; import LocalSettings from '../../models/LocalSettings'; +import { Brush, Brush2, Colorfilter, LanguageSquare, Rank, Ranking, Sound, Star1 } from 'iconsax-react-native'; const PreferencesView = () => { const dispatch = useDispatch(); const language = useSelector((state) => state.language.value); const settings = useSelector((state) => state.settings.local); return ( -
- - - + { }} elements={[ { + icon: , type: 'dropdown', title: translate('SettingsPreferencesTheme'), + description: 'Définissez le theme (Dark ou Light) de votre application', data: { value: settings.colorScheme, defaultValue: 'system', @@ -45,8 +51,10 @@ const PreferencesView = () => { }, }, { + icon: , type: 'dropdown', title: translate('SettingsPreferencesLanguage'), + description: 'Définissez la langue de votre application', data: { value: language, defaultValue: DefaultLanguage, @@ -61,8 +69,10 @@ const PreferencesView = () => { }, }, { + icon: , type: 'dropdown', title: translate('SettingsPreferencesDifficulty'), + description: 'La précision du tempo est de plus en plus élevée', data: { value: settings.difficulty, defaultValue: 'medium', @@ -90,8 +100,10 @@ const PreferencesView = () => { }} elements={[ { + icon: , type: 'toggle', title: translate('SettingsPreferencesColorblindMode'), + description: 'Augmente le contraste', data: { value: settings.colorBlind, onToggle: () => { @@ -109,8 +121,10 @@ const PreferencesView = () => { }} elements={[ { + icon: , type: 'range', title: translate('SettingsPreferencesMicVolume'), + description: 'Régler le volume de votre micro selon vos preference', data: { value: settings.micVolume, min: 0, @@ -139,7 +153,7 @@ const PreferencesView = () => { },*/ ]} /> -
+ ); }; diff --git a/front/views/settings/PrivacyView.tsx b/front/views/settings/PrivacyView.tsx index a541faf..726d6b2 100644 --- a/front/views/settings/PrivacyView.tsx +++ b/front/views/settings/PrivacyView.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Center, Heading } from 'native-base'; +import { Center, Flex, Heading } from 'native-base'; import { translate } from '../../i18n/i18n'; import ElementList from '../../components/GtkUI/ElementList'; import { useDispatch } from 'react-redux'; @@ -7,6 +7,7 @@ import { RootState, useSelector } from '../../state/Store'; import { updateSettings } from '../../state/SettingsSlice'; import useUserSettings from '../../hooks/userSettings'; import { LoadingView } from '../../components/Loading'; +import { Driver, Driver2, Like1, Shop } from 'iconsax-react-native'; const PrivacyView = () => { const dispatch = useDispatch(); @@ -17,9 +18,13 @@ const PrivacyView = () => { return ; } return ( -
- {translate('privBtn')} - + { elements={[ { type: 'toggle', + icon: , title: translate('dataCollection'), + description: 'Acceptez-vous la récupération de vos données pour l\'amélioration de Chromacase ?', data: { value: settings.dataCollection, onToggle: () => @@ -40,7 +47,9 @@ const PrivacyView = () => { }, { type: 'toggle', + icon: , title: translate('customAds'), + description: 'Afficher les suggestions dans la section des recommandations', data: { value: settings.customAds, onToggle: () => @@ -49,7 +58,9 @@ const PrivacyView = () => { }, { type: 'toggle', + icon: , title: translate('recommendations'), + description: 'Souhaitez-vous recevoir nos conseils et recommandations ?', data: { value: userSettings.data.recommendations, onToggle: () => @@ -60,7 +71,7 @@ const PrivacyView = () => { }, ]} /> -
+ ); }; diff --git a/front/views/settings/ProfileView.tsx b/front/views/settings/ProfileView.tsx new file mode 100644 index 0000000..fdf16d5 --- /dev/null +++ b/front/views/settings/ProfileView.tsx @@ -0,0 +1,264 @@ +import API from '../../API'; +import { useDispatch } from 'react-redux'; +import { unsetAccessToken } from '../../state/UserSlice'; +import React from 'react'; +import { Column, Text, Button, Box, Flex, Center, Heading, Popover, Toast } from 'native-base'; +import TextButton from '../../components/TextButton'; +import { LoadingView } from '../../components/Loading'; +import ElementList from '../../components/GtkUI/ElementList'; +import { translate } from '../../i18n/i18n'; +import { useQuery } from '../../Queries'; +import UserAvatar from '../../components/UserAvatar'; +import * as ImagePicker from 'expo-image-picker'; + +// Too painful to infer the settings-only, typed navigator. Gave up +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const ProfileSettings = ({ navigation }: { navigation: any }) => { + const userQuery = useQuery(API.getUserInfo); + const dispatch = useDispatch(); + + if (!userQuery.data || userQuery.isLoading) { + return ; + } + const user = userQuery.data; + return ( + + +
+ +
+ { + navigation.navigate('changeEmail'); + }, + }, + }, + { + type: 'text', + title: translate('verified'), + data: { + text: user.emailVerified ? 'verified' : 'not verified', + onPress: user.emailVerified + ? undefined + : () => API.fetch({ route: '/auth/reverify', method: 'PUT' }), + }, + }, + { + type: 'text', + title: translate('avatar'), + data: { + text: translate('changeIt'), + onPress: () => { + ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + aspect: [1, 1], + quality: 1, + base64: true, + }).then((result) => { + console.log(result); + const image = result.assets?.at(0); + + if (!result.canceled && image) { + API.updateProfileAvatar(image) + .then(() => { + userQuery.refetch(); + Toast.show({ + description: 'Update successful', + }); + }) + .catch((e) => { + console.error(e); + Toast.show({ description: 'Update failed' }); + }); + } + }); + }, + }, + }, + ]} + /> + + Unlink + // : , + }, + { + type: 'text', + title: translate('nbGamesPlayed'), + data: { + text: user.data.gamesPlayed.toString(), + }, + }, + { + type: 'text', + title: 'XP', + description: translate('XPDescription'), + data: { + text: user.data.xp.toString(), + }, + }, + { + type: 'text', + title: translate('userCreatedAt'), + helperText: + 'La date de création est actuellement arbitraire car le serveur ne retourne pas cette information', + data: { + text: user.data.createdAt.toLocaleDateString(), + }, + }, + { + type: 'text', + title: translate('premiumAccount'), + data: { + text: translate(user.premium ? 'yes' : 'no'), + }, + }, + ]} + /> + + Fonctionnalités premium + + {}, + }, + }, + { + type: 'dropdown', + title: 'Thème de piano', + disabled: true, + data: { + value: 'default', + onSelect: () => {}, + options: [ + { + label: 'Default', + value: 'default', + }, + { + label: 'Catpuccino', + value: 'catpuccino', + }, + ], + }, + }, + ]} + /> +
+ + + {!user.isGuest && ( + dispatch(unsetAccessToken())} + translate={{ + translationKey: 'signOutBtn', + }} + /> + )} + {user.isGuest && ( + ( + + )} + > + + + + + {translate('Attention')} + + + {translate('YouAreCurrentlyConnectedWithAGuestAccountWarning')} + + + + + + + + + )} + +
+ ); +}; + +export default ProfileSettings; diff --git a/front/views/settings/SettingsPremiumView.tsx b/front/views/settings/SettingsPremiumView.tsx new file mode 100644 index 0000000..cc64a4f --- /dev/null +++ b/front/views/settings/SettingsPremiumView.tsx @@ -0,0 +1,91 @@ +import API from '../../API'; +import { useDispatch } from 'react-redux'; +import { unsetAccessToken } from '../../state/UserSlice'; +import React from 'react'; +import { Column, Text, Button, Box, Flex, Center, Heading, Popover, Toast } from 'native-base'; +import TextButton from '../../components/TextButton'; +import { LoadingView } from '../../components/Loading'; +import ElementList from '../../components/GtkUI/ElementList'; +import { translate } from '../../i18n/i18n'; +import { useQuery } from '../../Queries'; +import UserAvatar from '../../components/UserAvatar'; +import * as ImagePicker from 'expo-image-picker'; +import SettingBase from '../../components/UI/SettingsBase'; +import { Designtools, Google, Magicpen, PasswordCheck, SmsEdit, Star1, UserSquare } from 'iconsax-react-native'; +import { LinearGradient } from 'expo-linear-gradient'; + +// Too painful to infer the settings-only, typed navigator. Gave up +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const PremiumSettings = () => { + const userQuery = useQuery(API.getUserInfo); + const dispatch = useDispatch(); + + if (!userQuery.data || userQuery.isLoading) { + return ; + } + const user = userQuery.data; + return ( + + , + type: 'text', + title: translate('premiumAccount'), + description: 'Personalisation premium et outils vous permetant de passer au niveau supperieur', + data: { + text: translate(user.premium ? 'yes' : 'no'), + }, + }, + { + icon: , + type: 'toggle', + title: 'Piano Magique', + description: 'Fait apparaître de la lumière sur le piano pendant les parties', + helperText: + 'Vous devez posséder le module physique lumineux Chromacase pour pouvoir utiliser cette fonctionnalité', + disabled: true, + data: { + value: false, + onToggle: () => {}, + }, + }, + { + icon: , + type: 'dropdown', + title: 'Thème de piano', + description: 'Définissez le theme de votre piano', + disabled: true, + data: { + value: 'default', + onSelect: () => {}, + options: [ + { + label: 'Default', + value: 'default', + }, + { + label: 'Catpuccino', + value: 'catpuccino', + }, + ], + }, + }, + ]} + /> + + ); +}; + +export default PremiumSettings; diff --git a/front/views/settings/SettingsProfileView.tsx b/front/views/settings/SettingsProfileView.tsx index fdf16d5..53dc7ea 100644 --- a/front/views/settings/SettingsProfileView.tsx +++ b/front/views/settings/SettingsProfileView.tsx @@ -2,7 +2,7 @@ import API from '../../API'; import { useDispatch } from 'react-redux'; import { unsetAccessToken } from '../../state/UserSlice'; import React from 'react'; -import { Column, Text, Button, Box, Flex, Center, Heading, Popover, Toast } from 'native-base'; +import { Column, Text, Button, Box, Flex, Center, Heading, Popover, Toast, View } from 'native-base'; import TextButton from '../../components/TextButton'; import { LoadingView } from '../../components/Loading'; import ElementList from '../../components/GtkUI/ElementList'; @@ -10,10 +10,27 @@ import { translate } from '../../i18n/i18n'; import { useQuery } from '../../Queries'; import UserAvatar from '../../components/UserAvatar'; import * as ImagePicker from 'expo-image-picker'; +import SettingBase from '../../components/UI/SettingsBase'; +import { ArrowDown2, EyeSlash, Google, Lock1, PasswordCheck, Sms, SmsEdit, UserSquare } from 'iconsax-react-native'; +import { LinearGradient } from 'expo-linear-gradient'; +import TextFormField from '../../components/UI/TextFormField'; +import ButtonBase from '../../components/UI/ButtonBase'; +import ChangeEmailForm from '../../components/forms/changeEmailForm'; +import ChangePasswordForm from '../../components/forms/changePasswordForm'; + +const handleChangeEmail = async (newEmail: string): Promise => { + await API.updateUserEmail(newEmail); + return translate('emailUpdated'); +}; + +const handleChangePassword = async (oldPassword: string, newPassword: string): Promise => { + await API.updateUserPassword(oldPassword, newPassword); + return translate('passwordUpdated'); +}; // Too painful to infer the settings-only, typed navigator. Gave up // eslint-disable-next-line @typescript-eslint/no-explicit-any -const ProfileSettings = ({ navigation }: { navigation: any }) => { +const ProfileSettings = () => { const userQuery = useQuery(API.getUserInfo); const dispatch = useDispatch(); @@ -26,237 +43,93 @@ const ProfileSettings = ({ navigation }: { navigation: any }) => { style={{ flex: 1, alignItems: 'center', - paddingTop: 40, + paddingTop: 32, }} > - -
- -
- { - navigation.navigate('changeEmail'); - }, - }, + elements={[ + { + icon: , + type: 'text', + title: "Google account", // TODO translate + description: "Liez votre compte Google à ChromaCase", // TODO translate + data: { + text: user.googleID ? 'Linked' : 'Not linked', }, - { - type: 'text', - title: translate('verified'), - data: { - text: user.emailVerified ? 'verified' : 'not verified', - onPress: user.emailVerified - ? undefined - : () => API.fetch({ route: '/auth/reverify', method: 'PUT' }), - }, - }, - { - type: 'text', - title: translate('avatar'), - data: { - text: translate('changeIt'), - onPress: () => { - ImagePicker.launchImageLibraryAsync({ - mediaTypes: ImagePicker.MediaTypeOptions.Images, - aspect: [1, 1], - quality: 1, - base64: true, - }).then((result) => { - console.log(result); - const image = result.assets?.at(0); + }, + { + icon: , + type: 'text', + title: translate('avatar'), + description: "Changer votre photo de profile", // TODO translate + data: { + text: translate('changeIt'), + onPress: () => { + ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + aspect: [1, 1], + quality: 1, + base64: true, + }).then((result) => { + console.log(result); + const image = result.assets?.at(0); - if (!result.canceled && image) { - API.updateProfileAvatar(image) - .then(() => { - userQuery.refetch(); - Toast.show({ - description: 'Update successful', - }); - }) - .catch((e) => { - console.error(e); - Toast.show({ description: 'Update failed' }); + if (!result.canceled && image) { + API.updateProfileAvatar(image) + .then(() => { + userQuery.refetch(); + Toast.show({ + description: 'Update successful', }); - } - }); - }, + }) + .catch((e) => { + console.error(e); + Toast.show({ description: 'Update failed' }); + }); + } + }); }, }, - ]} - /> - - Unlink - // : , - }, - { - type: 'text', - title: translate('nbGamesPlayed'), - data: { - text: user.data.gamesPlayed.toString(), - }, - }, - { - type: 'text', - title: 'XP', - description: translate('XPDescription'), - data: { - text: user.data.xp.toString(), - }, - }, - { - type: 'text', - title: translate('userCreatedAt'), - helperText: - 'La date de création est actuellement arbitraire car le serveur ne retourne pas cette information', - data: { - text: user.data.createdAt.toLocaleDateString(), - }, - }, - { - type: 'text', - title: translate('premiumAccount'), - data: { - text: translate(user.premium ? 'yes' : 'no'), - }, - }, - ]} - /> - - Fonctionnalités premium - - {}, - }, - }, - { - type: 'dropdown', - title: 'Thème de piano', - disabled: true, - data: { - value: 'default', - onSelect: () => {}, - options: [ - { - label: 'Default', - value: 'default', - }, - { - label: 'Catpuccino', - value: 'catpuccino', - }, - ], - }, - }, - ]} - /> -
- - - {!user.isGuest && ( - dispatch(unsetAccessToken())} - translate={{ - translationKey: 'signOutBtn', - }} - /> - )} - {user.isGuest && ( - ( - - )} - > - - - - - {translate('Attention')} - - - {translate('YouAreCurrentlyConnectedWithAGuestAccountWarning')} - - - - - - - - - )} - + }, + { + icon: , + type: 'sectionDropdown', + title: 'Change email', // TODO translate + description: "Saisissez votre adresse électronique actuelle et définissez votre nouvelle adresse électroniquetion", // TODO translate + data: { + value: true, + section: [ + + handleChangePassword(oldPassword, newPassword) + } + /> + ] + } + }, + { + icon: , + type: 'sectionDropdown', + title: 'Change password', // TODO translate + description: "Saisissez votre mot de passe actuel et définissez votre nouveau mot de passe", // TODO translate + data: { + value: true, + section: [ + + handleChangeEmail(newEmail) + } + /> + ] + } + }, + ]} + /> ); }; diff --git a/front/views/settings/SettingsView.tsx b/front/views/settings/SettingsView.tsx index a0350e1..b8a4d27 100644 --- a/front/views/settings/SettingsView.tsx +++ b/front/views/settings/SettingsView.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react'; -import { Center, Text, Heading, Box } from 'native-base'; +import { Center, Text, Heading, Box, Row } from 'native-base'; import { translate } from '../../i18n/i18n'; import createTabRowNavigator from '../../components/navigators/TabRowNavigator'; import { MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons'; @@ -13,46 +13,12 @@ import GuestToUserView from './GuestToUserView'; import { useQuery } from '../../Queries'; import API from '../../API'; import { RouteProps } from '../../Navigation'; - -const handleChangeEmail = async (newEmail: string): Promise => { - await API.updateUserEmail(newEmail); - return translate('emailUpdated'); -}; - -const handleChangePassword = async (oldPassword: string, newPassword: string): Promise => { - await API.updateUserPassword(oldPassword, newPassword); - return translate('passwordUpdated'); -}; - -export const ChangePasswordView = () => { - return ( -
- {translate('changePassword')} - - handleChangePassword(oldPassword, newPassword) - } - /> -
- ); -}; - -export const ChangeEmailView = () => { - return ( -
- {translate('changeEmail')} - handleChangeEmail(newEmail)} /> -
- ); -}; - -export const GoogleAccountView = () => { - return ( -
- GoogleAccount -
- ); -}; +import { PressableAndroidRippleConfig, StyleProp, TextStyle, View, ViewStyle, useWindowDimensions } from 'react-native'; +import { TabView, SceneMap, TabBar, NavigationState, Route, SceneRendererProps, TabBarIndicatorProps, TabBarItemProps } from 'react-native-tab-view'; +import { HeartEdit, Star1, UserEdit, Notification, SecurityUser, Music, Icon } from 'iconsax-react-native'; +import { Scene, Event } from 'react-native-tab-view/lib/typescript/src/types'; +import { LinearGradient } from 'expo-linear-gradient'; +import PremiumSettings from './SettingsPremiumView'; export const PianoSettingsView = () => { return ( @@ -62,125 +28,69 @@ export const PianoSettingsView = () => { ); }; -const TabRow = createTabRowNavigator(); +const renderScene = SceneMap({ + profile: ProfileSettings, + premium: PremiumSettings, + preferences: PreferencesView, + notifications: NotificationsView, + privacy: PrivacyView, + piano: PianoSettingsView, +}); -type SetttingsNavigatorProps = { - screen?: - | 'profile' - | 'preferences' - | 'notifications' - | 'privacy' - | 'changePassword' - | 'changeEmail' - | 'googleAccount' - | 'pianoSettings'; -}; +const SetttingsNavigator = () => { + const layout = useWindowDimensions(); -const SetttingsNavigator = (props?: RouteProps) => { - const userQuery = useQuery(API.getUserInfo); - const user = useMemo(() => userQuery.data, [userQuery]); + const [index, setIndex] = React.useState(0); + const [routes] = React.useState([ + {index: 0, key: 'profile', title: 'Profile', icon: UserEdit}, + {index: 1, key: 'premium', title: 'Premium', icon: Star1}, + {index: 2, key: 'preferences', title: 'Preferences', icon: HeartEdit}, + {index: 3, key: 'notifications', title: 'Notifications', icon: Notification}, + {index: 4, key: 'privacy', title: 'Privacy', icon: SecurityUser}, + {index: 5, key: 'piano', title: 'Piano', icon: Music}, + ]); - if (userQuery.isLoading) { - return ( -
- Loading... -
- ); - } + const renderTabBar = (props: JSX.IntrinsicAttributes & SceneRendererProps & { navigationState: NavigationState; scrollEnabled?: boolean | undefined; bounces?: boolean | undefined; activeColor?: string | undefined; inactiveColor?: string | undefined; pressColor?: string | undefined; pressOpacity?: number | undefined; getLabelText?: ((scene: Scene) => string | undefined) | undefined; getAccessible?: ((scene: Scene) => boolean | undefined) | undefined; getAccessibilityLabel?: ((scene: Scene) => string | undefined) | undefined; getTestID?: ((scene: Scene) => string | undefined) | undefined; renderLabel?: ((scene: Scene & { focused: boolean; color: string; }) => React.ReactNode) | undefined; renderIcon?: ((scene: Scene & { focused: boolean; color: string; }) => React.ReactNode) | undefined; renderBadge?: ((scene: Scene) => React.ReactNode) | undefined; renderIndicator?: ((props: TabBarIndicatorProps) => React.ReactNode) | undefined; renderTabBarItem?: ((props: TabBarItemProps & { key: string; }) => React.ReactElement>) | undefined; onTabPress?: ((scene: Scene & Event) => void) | undefined; onTabLongPress?: ((scene: Scene) => void) | undefined; tabStyle?: StyleProp; indicatorStyle?: StyleProp; indicatorContainerStyle?: StyleProp; labelStyle?: StyleProp; contentContainerStyle?: StyleProp; style?: StyleProp; gap?: number | undefined; testID?: string | undefined; android_ripple?: PressableAndroidRippleConfig | undefined; }) => ( + & { + focused: boolean; + color: string; + }) => { + const MyIcon: Icon = scene.route?.icon as unknown as Icon; + return scene.route?.index == index ? + + : + }} + renderLabel={({ route, focused, color }) => ( + layout.width > 750 ? + + {route.title} + : null + )} + tabStyle={{flexDirection: 'row'}} + /> + ); return ( - - {/* I'm doing this to be able to land on the summary of settings when clicking on settings and directly to the - wanted settings page if needed so I need to do special work with the 0 index */} - - {user && user.isGuest && ( - - )} - + - - - - - - - - +
); }; diff --git a/front/yarn.lock b/front/yarn.lock index 194e2f6..d2f0ae0 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -2468,6 +2468,11 @@ mkdirp "^1.0.4" rimraf "^3.0.2" +"@nyashanziramasanga/react-native-horizontal-scroll-menu@^1.0.11": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@nyashanziramasanga/react-native-horizontal-scroll-menu/-/react-native-horizontal-scroll-menu-1.0.11.tgz#e57c479a1a549034f49a23cc20710bb47ce48fed" + integrity sha512-5pqEpG9cqRoOZk6o8cGQN4PHbn1ceLpQvxBdY4dFAgffD8DHvSb9pS9MlmhAI0stvMWgacfef9fghcmjGYC4cg== + "@pmmmwh/react-refresh-webpack-plugin@^0.5.3": version "0.5.10" resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz#2eba163b8e7dbabb4ce3609ab5e32ab63dda3ef8" @@ -9118,10 +9123,10 @@ expo-keep-awake@~11.0.1: resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-11.0.1.tgz#ee354465892a94040ffe09901b85b469e7d54fb3" integrity sha512-44ZjgLE4lnce2d40Pv8xsjMVc6R5GvgHOwZfkLYtGmgYG9TYrEJeEj5UfSeweXPL3pBFhXKfFU8xpGYMaHdP0A== -expo-linear-gradient@^12.3.0: - version "12.3.0" - resolved "https://registry.yarnpkg.com/expo-linear-gradient/-/expo-linear-gradient-12.3.0.tgz#7abd8fedbf0138c86805aebbdfbbf5e5fa865f19" - integrity sha512-f9e+Oxe5z7fNQarTBZXilMyswlkbYWQHONVfq8MqmiEnW3h9XsxxmVJLG8uVQSQPUsbW+x1UUT/tnU6mkMWeLg== +expo-linear-gradient@~12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/expo-linear-gradient/-/expo-linear-gradient-12.0.1.tgz#452f793b0463ddf313aad431552f23acc85f5d64" + integrity sha512-TMl/wBTVQOliL4S3DS5Aa3UFfVySr0mdJEHLG6kfBdMCLkr+tfLI2rGyJ+scS7xgMsvhTIaurhf1+Z0sL3aLCg== expo-linking@~3.3.1: version "3.3.1" @@ -10764,6 +10769,13 @@ i18next@^21.8.16: dependencies: "@babel/runtime" "^7.17.2" +iconsax-react-native@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/iconsax-react-native/-/iconsax-react-native-0.0.8.tgz#7e80737c98acc63fbb3c327fcb48b1d9095b84c1" + integrity sha512-iEQku/mu5Tuq5hBdEtQ3nhQJ0iPSmJj1lUu8PibSD9kPgIesA/gPCqU0U29l78l+LOUMgus8eTJZ0+1LJxUo3Q== + dependencies: + prop-types "^15.7.2" + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -15832,6 +15844,13 @@ react-native-svg@^13.10.0: css-select "^5.1.0" css-tree "^1.1.3" +react-native-tab-view@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/react-native-tab-view/-/react-native-tab-view-3.5.2.tgz#2789b8af6148b16835869566bf13dc3b0e6c1b46" + integrity sha512-nE5WqjbeEPsWQx4mtz81QGVvgHRhujTNIIZiMCx3Bj6CBFDafbk7XZp9ocmtzXUQaZ4bhtVS43R4FIiR4LboJw== + dependencies: + use-latest-callback "^0.1.5" + react-native-testing-library@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/react-native-testing-library/-/react-native-testing-library-6.0.0.tgz#b6e1c2213008dc62bdf28bb1474dfaca9cece058"