Skip to content

Commit 86028c3

Browse files
committed
feat: Language selection
1 parent 9cdbd07 commit 86028c3

File tree

16 files changed

+221
-27
lines changed

16 files changed

+221
-27
lines changed

client/public/static/i18n/en/common.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"home": "Home",
33
"account": "Account",
44
"userList": "User list",
5-
"Examples": "Examples",
5+
"examples": "Examples",
66
"giveAdminRights": "Give admin rights",
77
"cancel": "Cancel",
88
"save": "Save",
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"users": "Users",
3+
"stat2": "Stat 2",
4+
"stat3": "Stat 3"
5+
}

client/public/static/i18n/en/user.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,8 @@
1111
"password": "Password",
1212
"passwordConfirm": "Confirm Password",
1313
"giveAdminRights": "Give admin rights",
14-
"newUserAdded": "New user added"
14+
"newUserAdded": "New user added",
15+
"lang": "Language",
16+
"fr": "French",
17+
"en": "English"
1518
}

client/public/static/i18n/fr/common.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"home": "Accueil",
33
"account": "Compte",
44
"userList": "Utilisateurs",
5-
"Examples": "Exemples",
5+
"examples": "Exemples",
66
"giveAdminRights": "Donner les droits admin",
77
"cancel": "Annuler",
88
"save": "Sauvegarder",
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"users": "Utilisateurs",
3+
"stat2": "Stat 2",
4+
"stat3": "Stat 3"
5+
}

client/public/static/i18n/fr/user.json

+6-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
"email": "Email",
99
"phone": "Téléphone",
1010
"password": "Mot de passe",
11-
"passwordConfirm": "Confirmer le mot de passe",
11+
"passwordConfirm": "Confirmer le mot de passe",
1212
"giveAdminRights": "Donner les droits d'administrateur",
13-
"newUserAdded": "Nouvel utilisateur ajouté"
14-
}
13+
"newUserAdded": "Nouvel utilisateur ajouté",
14+
"lang": "Langue",
15+
"fr": "Français",
16+
"en": "Anglais"
17+
}

client/src/api/HomeRoutes.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import Fetch from '../utils/fetcher';
2+
3+
const HomeRoutes = {
4+
stats: () => Fetch<{ users: number, stat2: number, stat3: number }>('/api/home/stats'),
5+
};
6+
7+
export default HomeRoutes;

client/src/components/forms/UserForm.tsx

+32-5
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
1-
import { Prisma } from '@fullstack-typescript-monorepo/prisma';
1+
import { DEFAULT_LANGUAGE, Language } from '@fullstack-typescript-monorepo/core';
2+
import { Lang } from '@fullstack-typescript-monorepo/prisma';
23
import { LoadingButton } from '@mui/lab';
3-
import { Box, Checkbox, Divider, FormControlLabel, Grid, TextField } from '@mui/material';
4+
import { Box, Checkbox, Divider, FormControl, FormControlLabel, Grid, InputLabel, MenuItem, Select, TextField } from '@mui/material';
45
import React from 'react';
56
import { useTranslation } from 'react-i18next';
67
import { useNavigate } from 'react-router';
78
import UserRoutes from '../../api/UserRoutes';
89
import { useAlert } from '../../hooks/useAlert';
10+
import { useAuth } from '../../hooks/useAuth';
911
import useForm from '../../hooks/useForm';
1012
import { useLoader } from '../../hooks/useLoader';
1113
import catchError from '../../utils/catchError';
1214

1315
interface Data {
1416
id?: number;
1517
admin: boolean;
18+
lang: Language;
1619
login: string;
1720
password: string;
1821
idperson?: number;
@@ -32,15 +35,17 @@ const UserForm = ({ data }: Props) => {
3235
const Loader = useLoader();
3336
const { t } = useTranslation('user');
3437
const navigate = useNavigate();
38+
const { user, updateData } = useAuth();
3539

3640
const { register, handleSubmit, formState: { isSubmitting }, reset } = useForm<Data>('user', {
3741
defaultValues: data,
3842
});
3943

4044
// Submit user data
4145
const onSubmit = async (formData: Data) => {
42-
const processedData: Prisma.UserUpdateInput = {
46+
const processedData = {
4347
admin: formData.admin,
48+
lang: formData.lang,
4449
login: formData.login,
4550
active: true,
4651
};
@@ -64,13 +69,25 @@ const UserForm = ({ data }: Props) => {
6469
update: personData,
6570
},
6671
}).then(() => {
72+
// Update user data if it's the current user
73+
if (user.id === formData.id) {
74+
updateData((prev) => ({
75+
...prev,
76+
...processedData,
77+
person: {
78+
...prev.person,
79+
...personData,
80+
},
81+
}));
82+
}
83+
6784
Alert.open('success', t('common:saved'));
6885
}).catch(catchError(Alert));
6986
Loader.close();
7087
} else { // Addition
71-
processedData.password = formData.password;
7288
await UserRoutes.insert({
7389
...processedData,
90+
password: formData.password,
7491
connexionToken: '',
7592
person: {
7693
create: personData,
@@ -87,12 +104,22 @@ const UserForm = ({ data }: Props) => {
87104
return (
88105
<form autoComplete="off" onSubmit={handleSubmit(onSubmit)}>
89106
<Grid container spacing={3} sx={{ pb: 2 }}>
90-
<Grid item xs={12}>
107+
<Grid item xs={12} sm={6}>
91108
<FormControlLabel
92109
control={<Checkbox {...register('admin', 'checkbox')} defaultChecked={data.admin} />}
93110
label={t('giveAdminRights')}
94111
/>
95112
</Grid>
113+
<Grid item xs={12} sm={6}>
114+
<FormControl fullWidth>
115+
<InputLabel>{t('lang')}</InputLabel>
116+
<Select {...register('lang', 'select', { required: true })} defaultValue={data.lang || DEFAULT_LANGUAGE}>
117+
{Object.keys(Lang).map((lang) => (
118+
<MenuItem key={lang} value={lang}>{t(lang)}</MenuItem>
119+
))}
120+
</Select>
121+
</FormControl>
122+
</Grid>
96123
<Grid item md={6} xs={12}>
97124
<TextField {...register('login', 'text', { required: true })} fullWidth />
98125
</Grid>

client/src/hooks/useAuth.tsx

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { DEFAULT_LANGUAGE } from '@fullstack-typescript-monorepo/core';
2-
import React, { useCallback, useContext, useMemo, useState } from 'react';
2+
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
33
import UserRoutes, { UserWithPerson } from '../api/UserRoutes';
4+
import { useLanguage } from './useLanguage';
45

56
interface AuthContextInterface {
67
user: UserWithPerson,
78
authed: boolean,
89
signin: (login: string, password: string) => Promise<UserWithPerson | null>,
910
signout: () => void,
10-
updateData: (data: UserWithPerson) => void,
11+
updateData: (data: React.SetStateAction<UserWithPerson>) => void,
1112
}
1213

1314
export const emptyUser: UserWithPerson = {
@@ -51,15 +52,22 @@ interface AuthProviderProps {
5152
}
5253

5354
export const AuthProvider = ({ children }: AuthProviderProps) => {
55+
const { setLanguage } = useLanguage();
5456
const [user, setUser] = useState<UserWithPerson>(emptyUser);
5557
const [authed, setAuthed] = useState(false);
5658

59+
// Update language when necessary
60+
useEffect(() => {
61+
setLanguage(user.lang);
62+
}, [user.lang, setLanguage]);
63+
5764
const signin = useCallback((
5865
login: string,
5966
password: string,
6067
) => UserRoutes.authenticate(login, password).then((response) => {
6168
localStorage.setItem('user', response.login);
6269
localStorage.setItem('token', response.connexionToken);
70+
6371
setUser(response);
6472
if (response) setAuthed(true);
6573
return response;
@@ -72,7 +80,7 @@ export const AuthProvider = ({ children }: AuthProviderProps) => {
7280
setUser(emptyUser);
7381
}, []);
7482

75-
const updateData = useCallback((data: UserWithPerson) => {
83+
const updateData = useCallback((data: React.SetStateAction<UserWithPerson>) => {
7684
setUser(data);
7785
}, []);
7886

client/src/layouts/DashboardLayout/NavBar/NavBar.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -51,22 +51,22 @@ const NavBar = ({ onMobileClose, openMobile }: Props) => {
5151
{
5252
href: '/app/todo',
5353
icon: Quiz,
54-
title: t('TODO'),
54+
title: t('TODO 1'),
5555
},
5656
{
5757
href: '/app/todo',
5858
icon: Quiz,
59-
title: t('TODO'),
59+
title: t('TODO 2'),
6060
},
6161
{
6262
href: '/app/todo',
6363
icon: Quiz,
64-
title: t('TODO'),
64+
title: t('TODO 3'),
6565
},
6666
{
6767
href: '/app/todo',
6868
icon: Quiz,
69-
title: t('TODO'),
69+
title: t('TODO 4'),
7070
},
7171
],
7272
},

client/src/views/HomeView.tsx

+91-7
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,101 @@
1-
import { Card, CardContent } from '@mui/material';
2-
import React from 'react';
1+
import { Euro, LocalAtm, Person2 } from '@mui/icons-material';
2+
import { Card, CardActionArea, CardContent, CardMedia, Grid } from '@mui/material';
3+
import React, { useEffect } from 'react';
34
import { useTranslation } from 'react-i18next';
5+
import HomeRoutes from '../api/HomeRoutes';
46
import Page from '../components/Page';
57
import Text from '../components/Text';
8+
import { useLoader } from '../hooks/useLoader';
9+
import useStateAsync from '../hooks/useStateAsync';
10+
11+
const display = {
12+
users: Person2,
13+
stat2: LocalAtm,
14+
stat3: Euro,
15+
} as const;
616

717
const HomeView = () => {
8-
const { t } = useTranslation();
18+
const { t } = useTranslation('home');
19+
const Loader = useLoader();
20+
21+
const { data } = useStateAsync(
22+
{ users: 0, stat2: 0, stat3: 0 },
23+
HomeRoutes.stats,
24+
null,
25+
);
26+
27+
// Close loader
28+
useEffect(() => {
29+
Loader.close();
30+
}, [Loader]);
931

1032
return (
11-
<Page title={t('home')}>
12-
<Card>
13-
<CardContent><Text>Hello !</Text></CardContent>
14-
</Card>
33+
<Page title={t('common:home')}>
34+
<Grid container spacing={2}>
35+
{(['users', 'stat2', 'stat3'] as const).map((key) => {
36+
const Icon = display[key];
37+
38+
return (
39+
<Grid item xs={12} sm={6} md={4} key={key}>
40+
<Card sx={{ textAlign: 'center', height: 1 }}>
41+
<CardActionArea
42+
sx={{ position: 'relative' }}
43+
>
44+
<CardMedia sx={{
45+
position: 'absolute',
46+
top: 0,
47+
left: 0,
48+
width: 1,
49+
height: 1,
50+
opacity: 0.1,
51+
p: 1,
52+
}}
53+
>
54+
<Icon sx={{ width: 1, height: 1 }} />
55+
</CardMedia>
56+
<CardContent>
57+
<Text h2 fontSize={40}>
58+
{data[key]}
59+
</Text>
60+
<Text caption>
61+
{t(key)}
62+
</Text>
63+
</CardContent>
64+
</CardActionArea>
65+
</Card>
66+
</Grid>
67+
);
68+
})}
69+
<Grid item xs={12} md={6}>
70+
<Card sx={{ height: 1 }}>
71+
<CardContent>
72+
<Text h6>Lorem ipsum</Text>
73+
<Text>
74+
Qui aliqua nulla occaecat consectetur adipisicing.
75+
Occaecat non exercitation veniam minim id est.
76+
Irure pariatur aute aliqua labore. Labore veniam qui id eiusmod incididunt
77+
excepteur magna. In dolore nostrud ex dolor incididunt.
78+
Sint adipisicing ea qui anim consectetur.
79+
</Text>
80+
</CardContent>
81+
</Card>
82+
</Grid>
83+
<Grid item xs={12} md={6}>
84+
<Card sx={{ height: 1 }}>
85+
<CardContent>
86+
<Text h6>Dolor sit amet</Text>
87+
<Text>
88+
Ut eu ipsum et irure do irure tempor. Magna consequat
89+
consequat esse adipisicing sint pariatur. Sit aliqua mollit irure
90+
nostrud consectetur magna velit ex veniam dolore est. Labore elit
91+
tempor dolor ullamco do voluptate ea labore aliquip tempor minim nisi
92+
reprehenderit minim. Exercitation pariatur elit ad mollit id ut nulla
93+
velit excepteur occaecat.
94+
</Text>
95+
</CardContent>
96+
</Card>
97+
</Grid>
98+
</Grid>
1599
</Page>
16100
);
17101
};

client/src/views/account/AccountView/ProfileDetails.tsx

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { DEFAULT_LANGUAGE } from '@fullstack-typescript-monorepo/core';
2+
import { Lang } from '@fullstack-typescript-monorepo/prisma';
13
import { LoadingButton } from '@mui/lab';
2-
import { Box, Card, CardContent, CardHeader, Divider, Grid, TextField } from '@mui/material';
4+
import { Box, Card, CardContent, CardHeader, Divider, FormControl, Grid, InputLabel, MenuItem, Select, TextField } from '@mui/material';
35
import React from 'react';
46
import { useWatch } from 'react-hook-form';
57
import { useTranslation } from 'react-i18next';
@@ -17,6 +19,7 @@ type FormData = {
1719
phone: string;
1820
password: string;
1921
passwordConfirm: string;
22+
lang: Lang;
2023
};
2124

2225
const ProfileDetails = ({ ...rest }) => {
@@ -31,6 +34,7 @@ const ProfileDetails = ({ ...rest }) => {
3134
lastName: auth.user.person.lastName,
3235
email: auth.user.person.email,
3336
phone: auth.user.person.phone || '',
37+
lang: auth.user.lang,
3438
},
3539
});
3640

@@ -58,6 +62,7 @@ const ProfileDetails = ({ ...rest }) => {
5862

5963
Loader.open();
6064
await UserRoutes.update(auth.user.id, {
65+
lang: data.lang,
6166
person: { update: processedData }
6267
}, { person: true }).then((newData) => {
6368
auth.updateData(newData as UserWithPerson);
@@ -97,6 +102,16 @@ const ProfileDetails = ({ ...rest }) => {
97102
fullWidth
98103
/>
99104
</Grid>
105+
<Grid item xs={12} sm={6}>
106+
<FormControl fullWidth>
107+
<InputLabel>{t('lang')}</InputLabel>
108+
<Select {...register('lang', 'select', { required: true })} defaultValue={auth.user.lang || DEFAULT_LANGUAGE}>
109+
{Object.keys(Lang).map((lang) => (
110+
<MenuItem key={lang} value={lang}>{t(lang)}</MenuItem>
111+
))}
112+
</Select>
113+
</FormControl>
114+
</Grid>
100115
</Grid>
101116
</CardContent>
102117
<Divider />

0 commit comments

Comments
 (0)