blob: db11fb6d22bc9f25c4573481f792bc44aa6f25c2 [file] [log] [blame]
simon26e79f72022-10-05 22:16:08 -04001/*
2 * Copyright (C) 2022 Savoir-faire Linux Inc.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Affero General Public License as
6 * published by the Free Software Foundation; either version 3 of the
7 * License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Affero General Public License for more details.
13 *
14 * You should have received a copy of the GNU Affero General Public
15 * License along with this program. If not, see
16 * <https://www.gnu.org/licenses/>.
17 */
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040018import { GppMaybe, Warning } from '@mui/icons-material';
19import {
20 IconButtonProps,
21 List,
22 ListItem,
23 ListItemIcon,
24 Stack,
25 TextField,
26 TextFieldProps,
27 Typography,
28} from '@mui/material';
simond47ef9e2022-09-28 22:24:28 -040029import { styled } from '@mui/material/styles';
simon35378692022-10-02 23:25:57 -040030import { ChangeEvent, ReactElement, useCallback, useEffect, useState } from 'react';
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -040031import { useTranslation } from 'react-i18next';
simon07b4eb02022-09-29 17:50:26 -040032
simon35378692022-10-02 23:25:57 -040033import { InfoButton, ToggleVisibilityButton } from './Button';
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040034import RulesDialog from './RulesDialog';
simon35378692022-10-02 23:25:57 -040035import { CheckedIcon, LockIcon, PenIcon, PersonIcon, RoundSaltireIcon } from './SvgIcon';
idillon-sfl37c18df2022-08-26 18:44:27 -040036
simond47ef9e2022-09-28 22:24:28 -040037const iconsHeight = '16px';
38const StyledCheckedIconSuccess = styled(CheckedIcon)(({ theme }) => ({
39 height: iconsHeight,
40 color: theme.palette.success.main,
41}));
42const StyledRoundSaltireIconError = styled(RoundSaltireIcon)(({ theme }) => ({
43 height: iconsHeight,
44 color: theme.palette.error.main,
45}));
46const StyledPenIconLight = styled(PenIcon)({ height: iconsHeight, color: '#03B9E9' });
47const StyledPenIconDark = styled(PenIcon)(({ theme }) => ({ height: iconsHeight, color: theme.palette.primary.dark }));
48const StyledPersonIconLight = styled(PersonIcon)({ height: iconsHeight, color: '#03B9E9' });
49const StyledLockIcon = styled(LockIcon)({ height: iconsHeight, color: '#03B9E9' });
idillon-sfl37c18df2022-08-26 18:44:27 -040050
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040051export type InputProps = TextFieldProps & {
simon35378692022-10-02 23:25:57 -040052 infoButtonProps?: IconButtonProps;
53 success?: boolean;
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040054 tooltipTitle: string;
simon35378692022-10-02 23:25:57 -040055};
56
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040057export const UsernameInput = ({
58 infoButtonProps,
59 onChange: _onChange,
60 success,
61 tooltipTitle,
62 ...props
63}: InputProps) => {
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -040064 const { t } = useTranslation();
simond47ef9e2022-09-28 22:24:28 -040065 const [isSelected, setIsSelected] = useState(false);
66 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -040067 const [startAdornment, setStartAdornment] = useState<ReactElement | undefined>();
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040068 const [isDialogOpened, setIsDialogOpened] = useState<boolean>(false);
idillon-sfl37c18df2022-08-26 18:44:27 -040069
simond47ef9e2022-09-28 22:24:28 -040070 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -040071 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -040072 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -040073 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -040074 },
simon80b7b3b2022-09-28 17:50:10 -040075 [_onChange]
simond47ef9e2022-09-28 22:24:28 -040076 );
idillon-sfl37c18df2022-08-26 18:44:27 -040077
simond47ef9e2022-09-28 22:24:28 -040078 useEffect(() => {
79 /* Handle startAdornment */
80 let Icon = StyledPersonIconLight;
81 let visibility = 'visible';
82 if (props.error) {
83 Icon = StyledRoundSaltireIconError;
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040084 } else if (success) {
simond47ef9e2022-09-28 22:24:28 -040085 Icon = StyledCheckedIconSuccess;
86 } else if (!isSelected && !input) {
87 visibility = 'hidden'; // keep icon's space so text does not move
idillon-sfl37c18df2022-08-26 18:44:27 -040088 }
simond47ef9e2022-09-28 22:24:28 -040089 setStartAdornment(<Icon sx={{ visibility }} />);
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040090 }, [props.error, success, isSelected, input]);
idillon-sfl37c18df2022-08-26 18:44:27 -040091
simond47ef9e2022-09-28 22:24:28 -040092 return (
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040093 <>
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -040094 <RulesDialog
95 openDialog={isDialogOpened}
96 title={t('username_rules_dialog_title')}
97 closeDialog={() => setIsDialogOpened(false)}
98 >
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040099 <UsernameRules />
100 </RulesDialog>
101 <TextField
102 {...props}
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400103 color={inputColor(props.error, success)}
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400104 label={t('username_input_label')}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400105 variant="standard"
106 InputLabelProps={{ shrink: !!(isSelected || input) }}
107 onChange={onChange}
108 InputProps={{
109 startAdornment,
110 endAdornment: (
111 <InfoButton tooltipTitle={tooltipTitle} {...infoButtonProps} onClick={() => setIsDialogOpened(true)} />
112 ),
113 }}
114 onFocus={() => setIsSelected(true)}
115 onBlur={() => setIsSelected(false)}
116 />
117 </>
simond47ef9e2022-09-28 22:24:28 -0400118 );
119};
idillon-sfl37c18df2022-08-26 18:44:27 -0400120
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400121export const PasswordInput = ({
122 infoButtonProps,
123 onChange: _onChange,
124 success,
125 tooltipTitle,
126 ...props
127}: InputProps) => {
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400128 const { t } = useTranslation();
simond47ef9e2022-09-28 22:24:28 -0400129 const [showPassword, setShowPassword] = useState(false);
130 const [isSelected, setIsSelected] = useState(false);
131 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400132 const [startAdornment, setStartAdornment] = useState<ReactElement | undefined>();
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400133 const [isDialogOpened, setIsDialogOpened] = useState<boolean>(false);
idillon-sfl37c18df2022-08-26 18:44:27 -0400134
simond47ef9e2022-09-28 22:24:28 -0400135 const toggleShowPassword = () => {
136 setShowPassword((showPassword) => !showPassword);
137 };
138
139 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400140 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400141 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400142 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400143 },
simon80b7b3b2022-09-28 17:50:10 -0400144 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400145 );
146
147 useEffect(() => {
148 /* Handle startAdornment */
149 let Icon = StyledLockIcon;
150 let visibility = 'visible';
151 if (props.error) {
152 Icon = StyledRoundSaltireIconError;
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400153 } else if (success) {
simond47ef9e2022-09-28 22:24:28 -0400154 Icon = StyledCheckedIconSuccess;
155 } else if (!isSelected && !input) {
156 visibility = 'hidden'; // keep icon's space so text does not move
157 }
158 setStartAdornment(<Icon sx={{ visibility }} />);
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400159 }, [props.error, success, isSelected, input]);
simond47ef9e2022-09-28 22:24:28 -0400160
161 return (
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400162 <>
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400163 <RulesDialog
164 openDialog={isDialogOpened}
165 title={t('password_rules_dialog_title')}
166 closeDialog={() => setIsDialogOpened(false)}
167 >
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400168 <PasswordRules />
169 </RulesDialog>
170 <TextField
171 {...props}
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400172 color={inputColor(props.error, success)}
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400173 label={t('password_input_label')}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400174 type={showPassword ? 'text' : 'password'}
175 variant="standard"
176 autoComplete="current-password"
177 InputLabelProps={{ shrink: !!(isSelected || input) }}
178 onChange={onChange}
179 InputProps={{
180 startAdornment,
181 endAdornment: (
182 <Stack direction="row" spacing="14px" alignItems="center">
183 <InfoButton tooltipTitle={tooltipTitle} {...infoButtonProps} onClick={() => setIsDialogOpened(true)} />
184 <ToggleVisibilityButton visible={showPassword} onClick={toggleShowPassword} />
185 </Stack>
186 ),
187 }}
188 onFocus={() => setIsSelected(true)}
189 onBlur={() => setIsSelected(false)}
190 />
191 </>
simond47ef9e2022-09-28 22:24:28 -0400192 );
193};
idillon-sfl37c18df2022-08-26 18:44:27 -0400194
simon35378692022-10-02 23:25:57 -0400195export const NickNameInput = ({ onChange: _onChange, ...props }: TextFieldProps) => {
simond47ef9e2022-09-28 22:24:28 -0400196 const [isSelected, setIsSelected] = useState(false);
197 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400198 const [startAdornmentVisibility, setStartAdornmentVisibility] = useState<'visible' | 'hidden'>('hidden');
idillon-sfl37c18df2022-08-26 18:44:27 -0400199
simond47ef9e2022-09-28 22:24:28 -0400200 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400201 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400202 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400203 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400204 },
simon80b7b3b2022-09-28 17:50:10 -0400205 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400206 );
idillon-sfl37c18df2022-08-26 18:44:27 -0400207
simond47ef9e2022-09-28 22:24:28 -0400208 useEffect(() => {
209 setStartAdornmentVisibility(isSelected || input ? 'visible' : 'hidden');
210 }, [isSelected, input]);
idillon-sfl37c18df2022-08-26 18:44:27 -0400211
simond47ef9e2022-09-28 22:24:28 -0400212 return (
213 <TextField
214 {...props}
215 label="Nickname, surname..."
216 variant="standard"
217 InputLabelProps={{ shrink: !!(isSelected || input) }}
218 onChange={onChange}
219 InputProps={{
220 startAdornment: <StyledPenIconLight sx={{ visibility: startAdornmentVisibility }} />,
221 }}
222 onFocus={() => setIsSelected(true)}
223 onBlur={() => setIsSelected(false)}
224 />
225 );
226};
idillon-sfl37c18df2022-08-26 18:44:27 -0400227
simon35378692022-10-02 23:25:57 -0400228export const RegularInput = ({ onChange: _onChange, ...props }: TextFieldProps) => {
simond47ef9e2022-09-28 22:24:28 -0400229 const [isSelected, setIsSelected] = useState(false);
230 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400231 const [startAdornmentVisibility, setStartAdornmentVisibility] = useState<'visible' | 'hidden'>('hidden');
232 const [endAdornmentVisibility, setEndAdornmentVisibility] = useState<'visible' | 'hidden'>('visible');
idillon-sfl37c18df2022-08-26 18:44:27 -0400233
simond47ef9e2022-09-28 22:24:28 -0400234 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400235 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400236 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400237 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400238 },
simon80b7b3b2022-09-28 17:50:10 -0400239 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400240 );
idillon-sfl37c18df2022-08-26 18:44:27 -0400241
simond47ef9e2022-09-28 22:24:28 -0400242 useEffect(() => {
243 setStartAdornmentVisibility(isSelected || input ? 'visible' : 'hidden');
244 setEndAdornmentVisibility(isSelected || input ? 'hidden' : 'visible');
245 }, [isSelected, input]);
idillon-sfl37c18df2022-08-26 18:44:27 -0400246
simond47ef9e2022-09-28 22:24:28 -0400247 return (
248 <TextField
249 {...props}
250 variant="standard"
251 InputLabelProps={{ shrink: !!(isSelected || input) }}
252 onChange={onChange}
253 InputProps={{
254 startAdornment: <StyledPenIconLight sx={{ visibility: startAdornmentVisibility }} />,
255 endAdornment: <StyledPenIconDark sx={{ visibility: endAdornmentVisibility }} />,
256 }}
257 onFocus={() => setIsSelected(true)}
258 onBlur={() => setIsSelected(false)}
259 />
260 );
261};
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400262
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400263function inputColor(
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400264 error?: boolean,
265 success?: boolean
266): 'success' | 'error' | 'primary' | 'secondary' | 'info' | 'warning' | undefined {
267 return error ? 'error' : success ? 'success' : 'primary';
268}
269
270const PasswordRules = () => {
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400271 const { t } = useTranslation();
272
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400273 return (
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400274 <List>
275 <ListItem>
276 <ListItemIcon>
277 <GppMaybe />
278 </ListItemIcon>
279 <Typography variant="body1">{t('password_rule_one')}</Typography>
280 </ListItem>
281 <ListItem>
282 <ListItemIcon>
283 <GppMaybe />
284 </ListItemIcon>
285 <Typography variant="body1">{t('password_rule_two')}</Typography>
286 </ListItem>
287 <ListItem>
288 <ListItemIcon>
289 <GppMaybe />
290 </ListItemIcon>
291 <Typography variant="body1">{t('password_rule_three')}</Typography>
292 </ListItem>
293 <ListItem>
294 <ListItemIcon>
295 <GppMaybe />
296 </ListItemIcon>
297 <Typography variant="body1">{t('password_rule_four')}</Typography>
298 </ListItem>
299 <ListItem>
300 <ListItemIcon>
301 <GppMaybe />
302 </ListItemIcon>
303 <Typography variant="body1">{t('password_rule_five')}</Typography>
304 </ListItem>
305 </List>
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400306 );
307};
308
309const UsernameRules = () => {
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400310 const { t } = useTranslation();
311
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400312 return (
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400313 <List>
314 <ListItem>
315 <ListItemIcon>
316 <Warning />
317 </ListItemIcon>
318 <Typography variant="body1">{t('username_rule_one')}</Typography>
319 </ListItem>
320 <ListItem>
321 <ListItemIcon>
322 <Warning />
323 </ListItemIcon>
324 <Typography variant="body1">{t('username_rule_two')}</Typography>
325 </ListItem>
326 <ListItem>
327 <ListItemIcon>
328 <Warning />
329 </ListItemIcon>
330 <Typography variant="body1">{t('username_rule_three')}</Typography>
331 </ListItem>
332 <ListItem>
333 <ListItemIcon>
334 <Warning />
335 </ListItemIcon>
336 <Typography variant="body1">{t('username_rule_four')}</Typography>
337 </ListItem>
338 </List>
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400339 );
340};