Refactor registration and login and improve routing

Changes:
- Improve registration and login pages
- Extract Home component from App.tsx file into its own
- Make Home component display registration or login
- Extract routes from App component and refactor routing

GitLab: #12
Change-Id: I68b01890781308282072b6dcf5e6df0d54837b4a
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 3e5de94..3f95af2 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -15,88 +15,21 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-import './dayjsInitializer';
+import { useState } from 'react';
+import { Outlet } from 'react-router-dom';
 
-import { ThemeProvider } from '@mui/material/styles';
-import { useEffect, useState } from 'react';
-import { Navigate, Route, Routes } from 'react-router-dom';
-
-import authManager from './AuthManager';
 import WelcomeAnimation from './components/welcome';
-import NotFoundPage from './pages/404';
-import AccountCreationDialog from './pages/AccountCreation';
-import AccountSelection from './pages/AccountSelection';
-import AccountSettings from './pages/AccountSettings';
-import JamiAccountDialog from './pages/JamiAccountCreation';
-import JamiMessenger from './pages/JamiMessenger';
-import SignInPage from './pages/LoginDialog';
-import ServerSetup from './pages/ServerSetup';
-import defaultTheme from './themes/Default';
-import { ThemeDemonstrator } from './themes/ThemeDemonstrator';
-
-// import { useSelector, useDispatch } from 'react-redux'
-// import { useAppSelector, useAppDispatch } from '../redux/hooks'
-
-const Home = () => {
-  return <Navigate to="/account" />;
-};
 
 const App = () => {
-  // const count = useSelector(state => state.counter.value)
-  // const dispatch = useDispatch();
-  // const count = useAppSelector((state) => state.counter.value);
-  // const dispatch = useAppDispatch();
-
-  const [state, setState] = useState({
-    loaded: false,
-    auth: authManager.getState(),
-  });
-  const [displayWelcome, setDisplayWelcome] = useState(true);
-
-  useEffect(() => {
-    authManager.init((auth) => {
-      setState({ loaded: false, auth });
-    });
-    return () => authManager.deinit();
-  }, []);
+  const [displayWelcome, setDisplayWelcome] = useState<boolean>(true);
 
   console.log('App render');
 
   if (displayWelcome) {
-    return <WelcomeAnimation showSetup={!state.auth.setupComplete} onComplete={() => setDisplayWelcome(false)} />;
-  } else if (!state.auth.setupComplete) {
-    return (
-      <Routes>
-        <Route path="/setup" element={<ServerSetup />} />
-        <Route path="/" element={<Navigate to="/setup" replace />} />
-        <Route path="*" element={<Navigate to="/setup" replace />} />
-      </Routes>
-    );
+    return <WelcomeAnimation onComplete={() => setDisplayWelcome(false)} />;
   }
 
-  return (
-    <ThemeProvider theme={defaultTheme}>
-      <Routes>
-        <Route path="/account">
-          <Route index element={<AccountSelection />} />
-          <Route path=":accountId">
-            <Route index element={<JamiMessenger />} />
-            <Route path="*" element={<JamiMessenger />} />
-            <Route path="settings" element={<AccountSettings />} />
-          </Route>
-        </Route>
-        <Route path="/newAccount" element={<AccountCreationDialog />}>
-          <Route path="jami" element={<JamiAccountDialog />} />
-        </Route>
-        {/* <Route path="/Contacts" element={<ContactList />} /> */}
-        <Route path="/Theme" element={<ThemeDemonstrator />} />
-        <Route path="/setup" element={<ServerSetup />} />
-        <Route path="/" element={<Home />} />
-        <Route path="*" element={<NotFoundPage />} />
-      </Routes>
-      {!state.auth.authenticated && <SignInPage key="signin" open={!state.auth.authenticated} />}
-    </ThemeProvider>
-  );
+  return <Outlet />;
 };
 
 export default App;
diff --git a/client/src/components/Input.tsx b/client/src/components/Input.tsx
index e867e26..ae640cf 100644
--- a/client/src/components/Input.tsx
+++ b/client/src/components/Input.tsx
@@ -88,6 +88,7 @@
       </RulesDialog>
       <TextField
         {...props}
+        color={inputColor(props.error, props.success)}
         label={'Choose an identifier'}
         variant="standard"
         InputLabelProps={{ shrink: !!(isSelected || input) }}
@@ -145,6 +146,7 @@
       </RulesDialog>
       <TextField
         {...props}
+        color={inputColor(props.error, props.success)}
         label="Password"
         type={showPassword ? 'text' : 'password'}
         variant="standard"
@@ -235,7 +237,7 @@
   );
 };
 
-export function inputColor(
+function inputColor(
   error?: boolean,
   success?: boolean
 ): 'success' | 'error' | 'primary' | 'secondary' | 'info' | 'warning' | undefined {
@@ -268,13 +270,13 @@
           <ListItemIcon>
             <GppMaybe />
           </ListItemIcon>
-          The password must contain at least one special character.
+          The password must contain at least 1 special character.
         </ListItem>
         <ListItem>
           <ListItemIcon>
             <GppMaybe />
           </ListItemIcon>
-          The password must be eight characters or longer for Strong strength.
+          The password must be 10 characters or longer to be considered strong.
         </ListItem>
       </List>
     </Typography>
diff --git a/client/src/components/JamiWelcomeLogo.tsx b/client/src/components/JamiWelcomeLogo.tsx
index 8002aab..31311b6 100644
--- a/client/src/components/JamiWelcomeLogo.tsx
+++ b/client/src/components/JamiWelcomeLogo.tsx
@@ -15,31 +15,29 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-import { Box, SxProps, Typography } from '@mui/material';
+import { Stack, StackProps, Typography } from '@mui/material';
 
 import { ReactComponent as JamiLogo } from '../icons/jami-logo-icon.svg';
 import { jamiLogoDefaultSize } from '../utils/constants';
 
-interface WelcomeLogoProps {
+interface WelcomeLogoProps extends StackProps {
   logoWidth?: string;
   logoHeight?: string;
-  boxSxProps?: SxProps;
 }
 
-export default function JamiWelcomeLogo(props: WelcomeLogoProps) {
+export default function JamiWelcomeLogo({ logoWidth, logoHeight, ...stackProps }: WelcomeLogoProps) {
   return (
-    <Box
+    <Stack
+      {...stackProps}
       sx={{
         display: 'flex',
         alignItems: 'center',
         justifyContent: 'center',
-        textAlign: 'center',
-        flexDirection: 'column',
-        ...props.boxSxProps,
+        ...stackProps.sx,
       }}
     >
-      <JamiLogo width={props.logoWidth ?? jamiLogoDefaultSize} height={props.logoHeight ?? jamiLogoDefaultSize} />
+      <JamiLogo width={logoWidth ?? jamiLogoDefaultSize} height={logoHeight ?? jamiLogoDefaultSize} />
       <Typography variant="h1">Welcome to Jami!</Typography>
-    </Box>
+    </Stack>
   );
 }
diff --git a/client/src/index.scss b/client/src/index.scss
index a9b9681..25ce911 100644
--- a/client/src/index.scss
+++ b/client/src/index.scss
@@ -1,5 +1,6 @@
 html,
-body {
+body,
+#root {
   height: 100%;
   margin: 0;
   padding: 0;
diff --git a/client/src/index.tsx b/client/src/index.tsx
index 83c2ada..90aa841 100644
--- a/client/src/index.tsx
+++ b/client/src/index.tsx
@@ -19,17 +19,24 @@
 import './index.scss';
 import './i18n';
 
-// import config from "../sentry-client.config.json"
+import { ThemeProvider } from '@mui/material/styles';
 import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
 import { StrictMode } from 'react';
 import { createRoot } from 'react-dom/client';
 import { Provider } from 'react-redux';
-import { BrowserRouter as Router } from 'react-router-dom';
+import { createBrowserRouter, createRoutesFromElements, Route, RouterProvider } from 'react-router-dom';
 import socketio from 'socket.io-client';
 
 import App from './App';
+import ContactList from './components/ContactList';
 import { SocketProvider } from './contexts/Socket';
+import AccountSettings from './pages/AccountSettings';
+import Home from './pages/Home';
+import JamiMessenger from './pages/JamiMessenger';
+import ServerSetup from './pages/ServerSetup';
 import { store } from './redux/store';
+import defaultTheme from './themes/Default';
+import { ThemeDemonstrator } from './themes/ThemeDemonstrator';
 
 const queryClient = new QueryClient({
   defaultOptions: {
@@ -41,6 +48,19 @@
 
 const socket = socketio();
 
+const router = createBrowserRouter(
+  createRoutesFromElements(
+    <Route path="/" element={<App />}>
+      <Route index element={<Home />} />
+      <Route path="theme" element={<ThemeDemonstrator />} />
+      <Route path="account" element={<JamiMessenger />} />
+      <Route path="settings" element={<AccountSettings />} />
+      <Route path="contacts" element={<ContactList />} />
+      <Route path="setup" element={<ServerSetup />} />
+    </Route>
+  )
+);
+
 const container = document.getElementById('root');
 if (!container) {
   throw new Error('Failed to get the root element');
@@ -51,9 +71,9 @@
     <StrictMode>
       <QueryClientProvider client={queryClient}>
         <SocketProvider socket={socket}>
-          <Router>
-            <App />
-          </Router>
+          <ThemeProvider theme={defaultTheme}>
+            <RouterProvider router={router} />
+          </ThemeProvider>
         </SocketProvider>
       </QueryClientProvider>
     </StrictMode>
diff --git a/client/src/locale/en/translation.json b/client/src/locale/en/translation.json
index 9a3982f..b2a2935 100644
--- a/client/src/locale/en/translation.json
+++ b/client/src/locale/en/translation.json
@@ -21,5 +21,16 @@
   "message_input_placeholder_two": "Write to {{member0}} and {{member1}}",
   "message_input_placeholder_three": "Write to {{member0}}, {{member1}} and {{member2}}",
   "message_input_placeholder_four": "Write to {{member0}}, {{member1}}, {{member2}}, +1 other member",
-  "message_input_placeholder_more": "Write to {{member0}}, {{member1}}, {{member2}}, +{{excess}} other members"
+  "message_input_placeholder_more": "Write to {{member0}}, {{member1}}, {{member2}}, +{{excess}} other members",
+  "username_input_default_helper_text": "",
+  "username_input_success_helper_text": "Username available",
+  "username_input_taken_helper_text": "Username already taken",
+  "username_input_invalid_helper_text": "Username doesn't follow required pattern",
+  "username_input_registration_failed_helper_text": "Username not correct!",
+  "password_input_default_helper_text": "",
+  "password_input_too_weak_helper_text": "Too weak",
+  "password_input_weak_helper_text": "Weak",
+  "password_input_medium_helper_text": "Medium",
+  "password_input_strong_helper_text": "Strong",
+  "password_input_registration_failed_helper_text": "Choose another password!"
 }
diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx
new file mode 100644
index 0000000..8a5e3d6
--- /dev/null
+++ b/client/src/pages/Home.tsx
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+import { Box, Grid, Paper, useMediaQuery } from '@mui/material';
+import { Theme, useTheme } from '@mui/material/styles';
+import { useState } from 'react';
+
+import JamiWelcomeLogo from '../components/JamiWelcomeLogo';
+import JamiLogin from './JamiLogin';
+import JamiRegistration from './JamiRegistration';
+
+const borderRadius = 30;
+
+export default function Home() {
+  const theme: Theme = useTheme();
+  const [isRegistrationDisplayed, setIsRegistrationDisplayed] = useState<boolean>(false);
+
+  const child = !isRegistrationDisplayed ? (
+    <JamiLogin register={() => setIsRegistrationDisplayed(true)} />
+  ) : (
+    <JamiRegistration login={() => setIsRegistrationDisplayed(false)} />
+  );
+
+  const isDesktopOrLaptop: boolean = useMediaQuery(theme.breakpoints.up('md'));
+  const isMobile: boolean = useMediaQuery(theme.breakpoints.only('xs'));
+
+  return (
+    <Box
+      sx={{
+        width: '100%',
+        height: '100%',
+        display: 'flex',
+        backgroundColor: `${isDesktopOrLaptop ? theme.palette.primary.dark : 'white'}`,
+      }}
+    >
+      <Paper
+        elevation={10}
+        sx={{
+          width: '100%',
+          backgroundColor: 'white',
+          margin: `${isDesktopOrLaptop ? theme.typography.pxToRem(100) : 0}`,
+          borderRadius: `${isDesktopOrLaptop ? theme.typography.pxToRem(borderRadius) : 0}`,
+        }}
+      >
+        <Grid container spacing={0} sx={{ height: '100%' }}>
+          {!isMobile && (
+            <Grid
+              item
+              xs={6}
+              id="logo"
+              sx={{
+                height: '100%',
+                backgroundColor: theme.palette.secondary.main,
+                borderRadius: `${
+                  isDesktopOrLaptop
+                    ? `${theme.typography.pxToRem(borderRadius)} 0 0 ${theme.typography.pxToRem(borderRadius)}`
+                    : 0
+                }`, // To follow paper border-radius
+              }}
+            >
+              <JamiWelcomeLogo logoWidth="90%" sx={{ height: '100%' }} />
+            </Grid>
+          )}
+          <Grid item xs={isMobile ? 12 : 6} sx={{ height: '100%' }}>
+            {isMobile && (
+              <JamiWelcomeLogo
+                logoWidth={theme.typography.pxToRem(100)}
+                logoHeight={theme.typography.pxToRem(100)}
+                sx={{ mt: theme.typography.pxToRem(30), mb: theme.typography.pxToRem(20) }}
+              />
+            )}
+            <Box className="home-child" sx={{ height: `${isMobile ? 'auto' : '100%'}` }}>
+              {child}
+            </Box>
+          </Grid>
+        </Grid>
+      </Paper>
+    </Box>
+  );
+}
diff --git a/client/src/pages/JamiAccountCreation.tsx b/client/src/pages/JamiAccountCreation.tsx
deleted file mode 100644
index da80533..0000000
--- a/client/src/pages/JamiAccountCreation.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright (C) 2022 Savoir-faire Linux Inc.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this program.  If not, see
- * <https://www.gnu.org/licenses/>.
- */
-import { AddRounded } from '@mui/icons-material';
-import { Box, Card, CardActions, CardContent, Container, Fab, Typography } from '@mui/material';
-import { FormEvent, useState } from 'react';
-import { useNavigate } from 'react-router';
-
-import authManager from '../AuthManager';
-import UsernameChooser from '../components/UsernameChooser';
-
-export default function JamiAccountDialog() {
-  const [name, setName] = useState('');
-  const [loading, setLoading] = useState(false);
-  const [error, setError] = useState(false);
-  const navigate = useNavigate();
-
-  const onSubmit = async (event: FormEvent) => {
-    event.preventDefault();
-    setLoading(true);
-    const result = await authManager
-      .fetch('/api/accounts', {
-        method: 'POST',
-        headers: {
-          Accept: 'application/json',
-          'Content-Type': 'application/json',
-        },
-        body: JSON.stringify({ registerName: name }),
-      })
-      .then((res) => res.json())
-      .catch((error) => {
-        setLoading(false);
-        setError(error);
-      });
-    console.log(result);
-    if (result && result.accountId) navigate(`/account/${result.accountId}/settings`);
-  };
-
-  return (
-    <Container>
-      <Card component="form" onSubmit={onSubmit}>
-        <CardContent>
-          <Typography gutterBottom variant="h5" component="h2">
-            Create Jami account
-          </Typography>
-          <Typography variant="body2" color="textSecondary" component="p">
-            Welcome to the Jami web node setup.
-            <br />
-            Let&apos;s start by creating a new administrator account to control access to the server configuration.
-          </Typography>
-
-          <Box>
-            <UsernameChooser disabled={loading} setName={setName} />
-          </Box>
-        </CardContent>
-        <CardActions>
-          {error && <Typography color="error">Error: {JSON.stringify(error)}</Typography>}
-          <Fab color="primary" type="submit" variant="extended" disabled={!name || loading}>
-            <AddRounded />
-            Register name
-          </Fab>
-        </CardActions>
-      </Card>
-    </Container>
-  );
-}
diff --git a/client/src/pages/JamiLogin.tsx b/client/src/pages/JamiLogin.tsx
new file mode 100644
index 0000000..66fcf3e
--- /dev/null
+++ b/client/src/pages/JamiLogin.tsx
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+import { Box, Button, Stack, Typography, useMediaQuery } from '@mui/material';
+import { Theme, useTheme } from '@mui/material/styles';
+import { ChangeEvent, FormEvent, MouseEvent, useState } from 'react';
+import { Form } from 'react-router-dom';
+
+import { PasswordInput, UsernameInput } from '../components/Input';
+import ProcessingRequest from '../components/ProcessingRequest';
+import { inputWidth } from '../utils/constants';
+
+type JamiLoginProps = {
+  register: () => void;
+};
+
+export default function JamiLogin(props: JamiLoginProps) {
+  const theme: Theme = useTheme();
+  const [username, setUsername] = useState<string>('');
+  const [password, setPassword] = useState<string>('');
+  const [isLoggingInUser, setIsLoggingInUser] = useState<boolean>(false);
+
+  const handleUsername = (event: ChangeEvent<HTMLInputElement>) => {
+    setUsername(event.target.value);
+  };
+
+  const handlePassword = (event: ChangeEvent<HTMLInputElement>) => {
+    setPassword(event.target.value);
+  };
+
+  const register = (event: MouseEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    props.register();
+  };
+
+  const authenticateUser = async (event: FormEvent) => {
+    event.preventDefault();
+    if (username.length > 0 && password.length > 0) {
+      setIsLoggingInUser(true);
+
+      // TODO: Replace with login logic (https://git.jami.net/savoirfairelinux/jami-web/-/issues/75).
+      await new Promise((resolve) => setTimeout(resolve, 2000));
+      console.log('Login');
+      setIsLoggingInUser(false);
+    }
+  };
+
+  const isMobile: boolean = useMediaQuery(theme.breakpoints.only('xs'));
+
+  return (
+    <>
+      <ProcessingRequest open={isLoggingInUser} />
+
+      <Stack
+        sx={{
+          minHeight: `${isMobile ? 'auto' : '100%'}`,
+          display: 'flex',
+          alignItems: 'center',
+          justifyContent: 'center',
+        }}
+      >
+        <Box sx={{ mt: theme.typography.pxToRem(50), mb: theme.typography.pxToRem(20) }}>
+          <Typography component={'span'} variant="h2">
+            LOGIN
+          </Typography>
+        </Box>
+
+        <Form method="post" id="login-form">
+          <div>
+            <UsernameInput
+              onChange={handleUsername}
+              tooltipTitle={'The username you registered with'}
+              sx={{ width: theme.typography.pxToRem(inputWidth) }}
+            />
+          </div>
+          <div>
+            <PasswordInput
+              onChange={handlePassword}
+              tooltipTitle={'The password you registered with'}
+              sx={{ width: theme.typography.pxToRem(inputWidth) }}
+            />
+          </div>
+
+          <Button
+            variant="contained"
+            type="submit"
+            onClick={authenticateUser}
+            sx={{ width: theme.typography.pxToRem(inputWidth), mt: theme.typography.pxToRem(20) }}
+          >
+            LOG IN
+          </Button>
+        </Form>
+
+        <Box sx={{ mt: theme.typography.pxToRem(50), mb: theme.typography.pxToRem(50) }}>
+          <Typography variant="body1">
+            Need an account ? &nbsp;
+            <a href="" onClick={register}>
+              REGISTER
+            </a>
+          </Typography>
+        </Box>
+      </Stack>
+    </>
+  );
+}
diff --git a/client/src/pages/JamiRegistration.tsx b/client/src/pages/JamiRegistration.tsx
new file mode 100644
index 0000000..ebfc5fd
--- /dev/null
+++ b/client/src/pages/JamiRegistration.tsx
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+import { Box, Button, Stack, Typography, useMediaQuery } from '@mui/material';
+import { Theme, useTheme } from '@mui/material/styles';
+import { ChangeEvent, FormEvent, MouseEvent, useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Form } from 'react-router-dom';
+
+import { PasswordInput, UsernameInput } from '../components/Input';
+import ProcessingRequest from '../components/ProcessingRequest';
+import { checkPasswordStrength, isNameRegistered, StrengthValueCode } from '../utils/auth';
+import { inputWidth, jamiUsernamePattern } from '../utils/constants';
+
+const usernameTooltipTitle =
+  'Choose a password hard to guess for others but easy to remember for you, ' +
+  'it must be at least 8 characters. ' +
+  "Your account won't be recovered if you forget it!\n\n" +
+  'Click for more details';
+
+const passwordTooltipTitle =
+  'Username may be from 3 to 32 chraracters long and contain a-z, A-Z, -, _\n\n' + 'Click for more details';
+
+type NameStatus = 'default' | 'success' | 'taken' | 'invalid' | 'registration_failed';
+type PasswordStatus = StrengthValueCode | 'registration_failed';
+
+type JamiRegistrationProps = {
+  login: () => void;
+};
+
+export default function JamiRegistration(props: JamiRegistrationProps) {
+  const theme: Theme = useTheme();
+  const { t } = useTranslation();
+
+  const [isCreatingUser, setIsCreatingUser] = useState(false);
+  const [usernameValue, setUsernameValue] = useState('');
+  const [passwordValue, setPasswordValue] = useState('');
+  const [usernameStatus, setUsernameStatus] = useState<NameStatus>('default');
+  const [passwordStatus, setPasswordStatus] = useState<PasswordStatus>('default');
+
+  const usernameError = usernameStatus !== 'success' && usernameStatus !== 'default';
+  const usernameSuccess = usernameStatus === 'success';
+  const passwordError = passwordStatus !== 'strong' && passwordStatus !== 'default';
+  const passwordSuccess = passwordStatus === 'strong';
+
+  useEffect(() => {
+    // To prevent lookup if field is empty, in error state or lookup already done
+    if (usernameValue.length > 0 && usernameStatus === 'default') {
+      const validateUsername = async () => {
+        if (await isNameRegistered(usernameValue)) {
+          setUsernameStatus('taken');
+        } else {
+          setUsernameStatus('success');
+        }
+      };
+      const timeout = setTimeout(validateUsername, 1000);
+
+      return () => clearTimeout(timeout);
+    }
+  }, [usernameValue, usernameStatus]);
+
+  const handleUsername = async (event: ChangeEvent<HTMLInputElement>) => {
+    const username: string = event.target.value;
+    setUsernameValue(username);
+
+    if (username.length > 0 && !jamiUsernamePattern.test(username)) {
+      setUsernameStatus('invalid');
+    } else {
+      setUsernameStatus('default');
+    }
+  };
+
+  const handlePassword = (event: ChangeEvent<HTMLInputElement>) => {
+    const password: string = event.target.value;
+    setPasswordValue(password);
+
+    if (password.length > 0) {
+      const checkResult = checkPasswordStrength(password);
+      setPasswordStatus(checkResult.valueCode);
+    } else {
+      setPasswordStatus('default');
+    }
+  };
+
+  const login = (event: MouseEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    props.login();
+  };
+
+  const handleSubmit = async (event: FormEvent) => {
+    event.preventDefault();
+    const canCreate = usernameSuccess && passwordSuccess;
+
+    if (canCreate) {
+      setIsCreatingUser(true);
+      // TODO: Replace with registration logic (https://git.jami.net/savoirfairelinux/jami-web/-/issues/75).
+      await new Promise((resolve) => setTimeout(resolve, 2000));
+      console.log('Account created');
+      setIsCreatingUser(false);
+    } else {
+      if (usernameError || usernameValue.length === 0) {
+        setUsernameStatus('registration_failed');
+      }
+      if (!passwordSuccess) {
+        setPasswordStatus('registration_failed');
+      }
+    }
+  };
+
+  const isMobile: boolean = useMediaQuery(theme.breakpoints.only('xs'));
+
+  return (
+    <>
+      <ProcessingRequest open={isCreatingUser} />
+
+      <Stack
+        sx={{
+          minHeight: `${isMobile ? 'auto' : '100%'}`,
+          display: 'flex',
+          alignItems: 'center',
+          justifyContent: 'center',
+        }}
+      >
+        <Box sx={{ mt: theme.typography.pxToRem(50), mb: theme.typography.pxToRem(20) }}>
+          <Typography component={'span'} variant="h2">
+            REGISTRATION
+          </Typography>
+        </Box>
+
+        <Form method="post" id="register-form">
+          <div>
+            <UsernameInput
+              value={usernameValue}
+              onChange={handleUsername}
+              error={usernameError}
+              success={usernameSuccess}
+              helperText={t(`username_input_${usernameStatus}_helper_text`)}
+              sx={{ width: theme.typography.pxToRem(inputWidth) }}
+              tooltipTitle={usernameTooltipTitle}
+            />
+          </div>
+          <div>
+            <PasswordInput
+              value={passwordValue}
+              onChange={handlePassword}
+              error={passwordError}
+              success={passwordSuccess}
+              helperText={t(`password_input_${passwordStatus}_helper_text`)}
+              sx={{ width: theme.typography.pxToRem(inputWidth) }}
+              tooltipTitle={passwordTooltipTitle}
+            />
+          </div>
+
+          <Button
+            variant="contained"
+            type="submit"
+            onClick={handleSubmit}
+            sx={{ width: theme.typography.pxToRem(inputWidth), mt: theme.typography.pxToRem(20) }}
+          >
+            REGISTER
+          </Button>
+        </Form>
+
+        <Box sx={{ mt: theme.typography.pxToRem(50), mb: theme.typography.pxToRem(50) }}>
+          <Typography variant="body1">
+            Already have an account ? &nbsp;
+            <a href="" onClick={login}>
+              LOG IN
+            </a>
+          </Typography>
+        </Box>
+      </Stack>
+    </>
+  );
+}
diff --git a/client/src/pages/LoginDialog.tsx b/client/src/pages/LoginDialog.tsx
deleted file mode 100644
index ee990bc..0000000
--- a/client/src/pages/LoginDialog.tsx
+++ /dev/null
@@ -1,204 +0,0 @@
-/*
- * Copyright (C) 2022 Savoir-faire Linux Inc.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this program.  If not, see
- * <https://www.gnu.org/licenses/>.
- */
-import Button from '@mui/material/Button';
-import Checkbox from '@mui/material/Checkbox';
-import Dialog from '@mui/material/Dialog';
-import DialogActions from '@mui/material/DialogActions';
-import DialogContent from '@mui/material/DialogContent';
-import DialogTitle from '@mui/material/DialogTitle';
-import FormControlLabel from '@mui/material/FormControlLabel';
-import TextField from '@mui/material/TextField';
-import { ChangeEvent, Component, MouseEvent } from 'react';
-
-import authManager from '../AuthManager';
-
-type SignInPageProps = {
-  open: boolean;
-};
-type SignInPageState = {
-  username?: string;
-  password?: string;
-  submitted?: boolean;
-  loading?: boolean;
-  redirect?: boolean;
-  error?: boolean;
-  open?: boolean;
-  errorMessage?: string;
-};
-
-class SignInPage extends Component<SignInPageProps, SignInPageState> {
-  constructor(props: SignInPageProps) {
-    console.log('SignInPage ' + props.open);
-    super(props);
-    this.state = {
-      submitted: false,
-      loading: false,
-    };
-    this.handleSubmit = this.handleSubmit.bind(this);
-    this.localLogin = this.localLogin.bind(this);
-  }
-
-  handleusername(text: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
-    this.setState({ username: text.target.value });
-  }
-
-  handlePassword(text: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
-    this.setState({ password: text.target.value });
-  }
-
-  localLogin() {
-    this.setState({
-      submitted: true,
-      loading: true,
-    });
-    authManager.authenticate('admin', 'admin');
-    /*fetch('/api/localLogin?username=none&password=none', {
-            header: { "Content-Type": "application/json" },
-            method: "POST",
-            credentials: 'same-origin'
-            //body: JSON.stringify({ obj })
-        })
-            .then((res) => {
-                if (res.status === '200') {
-                    this.setState({
-                        redirect: true
-                    });
-                } else if (res.status === '401') {
-                    this.setState({
-                        loading: false,
-                        error: true,
-                        open: true,
-                        errorMessage: "Wrong credentials! Your are not allowed to connect"
-                    })
-                }
-                //this.setState({ session: res });
-            }).catch((e) => {
-                this.setState({
-                    loading: false,
-                    error: true,
-                    open: true,
-                    errorMessage: e.toString()
-                })
-            })*/
-  }
-
-  handleSubmit(event: MouseEvent<HTMLButtonElement>) {
-    event.preventDefault();
-    const obj = {
-      username: this.state.username,
-      password: this.state.password,
-    };
-
-    this.setState({
-      submitted: true,
-      loading: true,
-    });
-
-    fetch('/api/login?username=' + obj.username + '&password=' + obj.password, {
-      headers: {
-        'Content-Type': 'application/json',
-      },
-      method: 'POST',
-      credentials: 'same-origin',
-      //body: JSON.stringify({ obj })
-    })
-      .then((res) => {
-        if (res.status === 200) {
-          this.setState({
-            redirect: true,
-          });
-        } else if (res.status === 401) {
-          this.setState({
-            loading: false,
-            error: true,
-            open: true,
-            errorMessage: 'Wrong credentials! Your are not allowed to connect',
-          });
-        }
-        //this.setState({ session: res });
-      })
-      .catch((e) => {
-        this.setState({
-          loading: false,
-          error: true,
-          open: true,
-          errorMessage: e.toString(),
-        });
-      });
-  }
-
-  render() {
-    console.log('SignInPage render ' + this.props.open);
-    return (
-      <Dialog open={this.props.open}>
-        <DialogTitle>Se connecter</DialogTitle>
-        <DialogContent>
-          <Button
-            type="submit"
-            fullWidth
-            variant="contained"
-            color="primary"
-            className="" /*{classes.submit}*/
-            onClick={() => {
-              this.localLogin();
-            }}
-          >
-            Compte local
-          </Button>
-          <TextField
-            variant="outlined"
-            margin="normal"
-            required
-            fullWidth
-            id="username"
-            label="LDAP Savoir-faire Linux"
-            name="username"
-            autoComplete="email"
-            autoFocus
-            onChange={(text) => {
-              this.handleusername(text);
-            }}
-          />
-          <TextField
-            variant="outlined"
-            margin="normal"
-            required
-            fullWidth
-            name="password"
-            label="Mot de passe"
-            type="password"
-            id="password"
-            autoComplete="current-password"
-            onChange={(text) => {
-              this.handlePassword(text);
-            }}
-          />
-          <FormControlLabel control={<Checkbox value="remember" color="primary" />} label="Se rapeller de moi" />
-        </DialogContent>
-
-        <DialogActions>
-          <Button type="submit" size="medium" onClick={this.handleSubmit}>
-            Se connecter
-          </Button>
-        </DialogActions>
-      </Dialog>
-    );
-  }
-}
-
-export default SignInPage;
diff --git a/client/src/themes/Default.ts b/client/src/themes/Default.ts
index c0f5884..60fb1ec 100644
--- a/client/src/themes/Default.ts
+++ b/client/src/themes/Default.ts
@@ -46,6 +46,9 @@
     success: {
       main: '#009980',
     },
+    secondary: {
+      main: '#A3C2DA',
+    },
   },
   InfoTooltip: {
     backgroundColor: {
diff --git a/client/src/utils/auth.ts b/client/src/utils/auth.ts
index 8d4d863..98ecdba 100644
--- a/client/src/utils/auth.ts
+++ b/client/src/utils/auth.ts
@@ -29,9 +29,13 @@
 
 export interface PasswordCheckResult {
   strong: boolean;
-  value: string;
+  valueCode: StrengthValueCode;
 }
 
+export type StrengthValueCode = 'default' | 'too_weak' | 'weak' | 'medium' | 'strong';
+
+const idToStrengthValueCode: StrengthValueCode[] = ['too_weak', 'weak', 'medium', 'strong'];
+
 // TODO: Find a way to do it differently or remove this check from account creation.
 // It doesn't work if the server has secured this path, so I tweaked the server for test.
 // The tweak is to remove secured of apiRouter middleware in the server (app.ts).
@@ -41,8 +45,10 @@
     if (response.status === HttpStatusCode.Ok) {
       const data: LookupResolveValue = await response.json();
       return data.name === name;
+    } else if (response.status === HttpStatusCode.NotFound) {
+      return false;
     }
-    return false;
+    return true;
   } catch (err) {
     return true;
   }
@@ -53,7 +59,7 @@
 
   const checkResult: PasswordCheckResult = {
     strong: strengthResult.id === PasswordStrength.Strong.valueOf(),
-    value: strengthResult.value,
+    valueCode: idToStrengthValueCode[strengthResult.id] ?? 'default',
   };
 
   return checkResult;
diff --git a/client/src/utils/constants.ts b/client/src/utils/constants.ts
index 06d1b92..341622f 100644
--- a/client/src/utils/constants.ts
+++ b/client/src/utils/constants.ts
@@ -15,7 +15,7 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-export const jamiUsernamePattern = '^[a-zA-Z0-9-_]{3,32}$';
+export const jamiUsernamePattern = /^[a-zA-Z0-9-_]{3,32}$/;
 
 export const inputWidth = 260;