all repos — caroster @ 116dd85d56ab607173ef9b2215698b7d24adb3fb

[Octree] Group carpool to your event https://caroster.io

feat: ✨ Improve auth pages

#336
Simon Mulquin simon@octree.ch
Fri, 07 Oct 2022 09:47:45 +0000
commit

116dd85d56ab607173ef9b2215698b7d24adb3fb

parent

3305d6f0b36993b4459c0e235b35274a1dda4b51

M backend/src/extensions/users-permissions/content-types/user/schema.jsonbackend/src/extensions/users-permissions/content-types/user/schema.json

@@ -100,8 +100,17 @@ "default": false

}, "lang": { "type": "enumeration", - "enum": ["fr", "en", "FR", "EN"], + "enum": [ + "fr", + "en", + "FR", + "EN" + ], "default": "fr" + }, + "newsletterConsent": { + "type": "boolean", + "default": true } } }
M backend/src/graphql/auth/index.tsbackend/src/graphql/auth/index.ts

@@ -6,6 +6,7 @@ definition(t) {

t.string("firstName"); t.string("lastName"); t.string("lang"); + t.boolean("newsletterConsent"); }, }), ],
D frontend/containers/LoginGoogle/index.js

@@ -1,37 +0,0 @@

-import CardContent from '@material-ui/core/CardContent'; -import {useTranslation} from 'react-i18next'; -import {makeStyles} from '@material-ui/core/styles'; -import Button from '@material-ui/core/Button'; - -const LoginGoogle = () => { - const {t} = useTranslation(); - const classes = useStyles(); - - return ( - <CardContent className={classes.content}> - <Button - variant="outlined" - color="primary" - href="/api/connect/google" - startIcon={<img src="/assets/google-icon.svg" alt="Google Login" />} - > - {t('signin.withGoogle')} - </Button> - </CardContent> - ); -}; - -const useStyles = makeStyles(theme => ({ - content: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - padding: theme.spacing(4, 0), - }, - googleIcon: { - height: 32, - width: 32, - }, -})); - -export default LoginGoogle;
A frontend/containers/LoginGoogle/index.tsx

@@ -0,0 +1,34 @@

+import {useTranslation} from 'react-i18next'; +import Button from '@material-ui/core/Button'; +import {makeStyles} from '@material-ui/core'; +import theme from '../../theme'; + +const LoginGoogle = () => { + const {t} = useTranslation(); + const classes = useStyles(); + + return ( + <Button + variant="outlined" + color="primary" + fullWidth + href="/api/connect/google" + > + <img + className={classes.googleLogo} + height="25px" + src="/assets/google-icon.svg" + alt="Google Login" + /> + {t('signin.withGoogle')} + </Button> + ); +}; + +const useStyles = makeStyles((theme) => ({ + googleLogo: { + marginRight: theme.spacing(1) + } +})); + +export default LoginGoogle;
A frontend/containers/MailSignUpForm/SignupActions.tsx

@@ -0,0 +1,29 @@

+import Link from 'next/link'; +import Button from '@material-ui/core/Button'; +import CardActions from '@material-ui/core/CardActions'; +import {useTranslation} from 'react-i18next'; +import {makeStyles} from '@material-ui/core/styles'; + +const SignUpActions = () => { + const {t} = useTranslation(); + const classes = useStyles(); + + return ( + <CardActions className={classes.actions}> + <Link href="/auth/login" passHref> + <Button id="SignUpLogin" variant="text"> + {t('signup.login')} + </Button> + </Link> + </CardActions> + ); +}; + +const useStyles = makeStyles(theme => ({ + actions: { + justifyContent: 'center', + marginBottom: theme.spacing(2), + }, +})); + +export default SignUpActions;
A frontend/containers/MailSignUpForm/index.tsx

@@ -0,0 +1,213 @@

+import {useState, useMemo} from 'react'; +import Box from '@material-ui/core/Box'; +import Divider from '@material-ui/core/Divider'; +import Checkbox from '@material-ui/core/Checkbox'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import TextField from '@material-ui/core/TextField'; +import Button from '@material-ui/core/Button'; +import CardContent from '@material-ui/core/CardContent'; +import Typography from '@material-ui/core/Typography'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import {useTranslation} from 'react-i18next'; +import {useRouter} from 'next/router'; +import {makeStyles} from '@material-ui/core/styles'; +import useToastsStore from '../../stores/useToastStore'; +import {useRegisterMutation} from '../../generated/graphql'; +import SignUpActions from './SignupActions'; + +const SignUp = () => { + const {t, i18n} = useTranslation(); + const classes = useStyles(); + const addToast = useToastsStore(s => s.addToast); + const router = useRouter(); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(''); + const [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [newsletterConsent, setNewsletterConsent] = useState(false); + const [register] = useRegisterMutation(); + + const canSubmit = useMemo( + () => + [firstName, lastName, email, password].filter(s => s.length < 2) + .length === 0, + [firstName, lastName, email, password] + ); + + const onSubmit = async e => { + e.preventDefault?.(); + if (isLoading) return; + setIsLoading(true); + try { + const lang = i18n.language.toUpperCase(); + await register({ + variables: { + user: { + username: email, + email, + password, + firstName, + lastName, + newsletterConsent, + lang, + }, + }, + }); + router.push('/auth/confirm'); + } catch (error) { + const strapiError = error.message; + console.error({strapiError}); + if (strapiError === 'Email or Username are already taken') + setError(t('signup.errors.email_taken')); + else addToast(t(`generic.errors.unknown`)); + } + setIsLoading(false); + return false; + }; + + return ( + <form onSubmit={onSubmit}> + <CardContent className={classes.content}> + <Typography + variant="overline" + component="h5" + align="center" + className={classes.lineBreak} + > + {t('signup.createForm')} + </Typography> + <Box className={classes.content}> + <TextField + label={t('signup.firstName')} + fullWidth + autoFocus + margin="dense" + value={firstName} + required={true} + onChange={({target: {value = ''}}) => setFirstName(value)} + id="SignUpFirstName" + name="firstName" + /> + <TextField + label={t('signup.lastName')} + fullWidth + required={true} + margin="dense" + value={lastName} + onChange={({target: {value = ''}}) => setLastName(value)} + id="SignUpLastName" + name="lastName" + /> + <TextField + label={t('signup.email')} + fullWidth + required={true} + error={!!error} + helperText={error} + margin="dense" + value={email} + onChange={({target: {value = ''}}) => setEmail(value)} + id="SignUpEmail" + name="email" + type="email" + /> + <TextField + label={t('signup.password')} + fullWidth + required={true} + margin="dense" + value={password} + onChange={({target: {value = ''}}) => setPassword(value)} + id="SignUpEmail" + name="password" + type="password" + /> + </Box> + <FormControlLabel + className={classes.newsletter} + control={ + <Checkbox + className={classes.checkbox} + color="primary" + value={newsletterConsent} + onChange={({target: {checked = false}}) => + setNewsletterConsent(checked) + } + /> + } + label={t('signup.newsletter.consent')} + /> + + <Box className={classes.content}> + <Button + color="primary" + variant="contained" + fullWidth + type="submit" + disabled={!canSubmit} + className={classes.button} + aria-disabled={!canSubmit} + id="SignUpSubmit" + endIcon={ + isLoading && ( + <CircularProgress + className={classes.loader} + size={20} + color="secondary" + /> + ) + } + > + {t('signup.submit')} + </Button> + </Box> + <Box className={classes.divider}> + <Divider /> + </Box> + <Typography align="center" variant="body2"> + {t('signup.account_already')} + </Typography> + </CardContent> + <SignUpActions /> + </form> + ); +}; + +const useStyles = makeStyles(theme => ({ + content: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + width: '100%', + padding: theme.spacing(0, 4), + }, + lineBreak: { + whiteSpace: 'pre-line', + lineHeight: 1.8, + paddingBottom: theme.spacing(4), + }, + loader: { + marginLeft: '14px', + color: theme.palette.background.paper, + }, + button: { + margin: theme.spacing(1), + }, + divider: { + width: '100%', + textAlign: 'center', + margin: theme.spacing(2, 0), + }, + newsletter: { + width: '100%', + margin: theme.spacing(2, 0), + }, + checkbox: { + padding: 0, + marginRight: theme.spacing(2), + }, +})); + +export default SignUp;
M frontend/containers/SignInForm/index.tsxfrontend/containers/SignInForm/index.tsx

@@ -12,6 +12,9 @@ import {useTranslation} from 'react-i18next';

import {signIn} from 'next-auth/react'; import useAddToEvents from '../../hooks/useAddToEvents'; import useRedirectUrlStore from '../../stores/useRedirectUrl'; +import LoginGoogle from '../LoginGoogle'; +import Divider from '@material-ui/core/Divider'; +import Box from '@material-ui/core/Box'; interface Props { error?: string;

@@ -51,51 +54,50 @@

return ( <form onSubmit={onSubmit}> <CardContent className={classes.content}> - <Typography variant="h6">{t('signin.title')}</Typography> + <Typography variant="h6" align="center"> + {t('signin.title')} + </Typography> {error && ( <FormHelperText error={true}> {t(`signin.errors.${error}`)} </FormHelperText> )} - <TextField - label={t('signin.email')} - fullWidth - required={true} - margin="dense" - value={email} - onChange={({target: {value = ''}}) => setEmail(value)} - id="SignInEmail" - name="email" - type="email" - error={!!error} - /> - <TextField - label={t('signin.password')} - fullWidth - required={true} - margin="dense" - value={password} - onChange={({target: {value = ''}}) => setPassword(value)} - id="SignInEmail" - name="password" - type="password" - error={!!error} - /> - <NextLink href="/auth/lost-password" passHref> - <Link> - <Typography align="center" variant="body2"> - {t('lost_password.message')} - </Typography> - </Link> - </NextLink> - </CardContent> - <CardActions className={classes.actions} align="center"> - <NextLink href="/auth/register" passHref> - <Button size="small" id="SignInRegister"> - {t('signin.register')} - </Button> - </NextLink> + <Box className={classes.content}> + <TextField + label={t('signin.email')} + fullWidth + required={true} + margin="dense" + value={email} + onChange={({target: {value = ''}}) => setEmail(value)} + id="SignInEmail" + name="email" + type="email" + error={!!error} + /> + <TextField + label={t('signin.password')} + fullWidth + required={true} + margin="dense" + value={password} + onChange={({target: {value = ''}}) => setPassword(value)} + id="SignInEmail" + name="password" + type="password" + error={!!error} + /> + </Box> + <Box className={classes.divider}> + <NextLink href="/auth/lost-password" passHref> + <Link> + <Typography align="center" variant="body2"> + {t('lost_password.message')} + </Typography> + </Link> + </NextLink> + </Box> <Button color="primary" variant="contained"

@@ -106,6 +108,23 @@ id="SignInSubmit"

> {t('signin.login')} </Button> + <Box className={classes.divider}> + <Typography>{t('signin.or')}</Typography> + </Box> + <LoginGoogle /> + <Box className={classes.divider}> + <Divider /> + </Box> + <Typography align="center" variant="body2"> + {t('signin.no_account')} + </Typography> + </CardContent> + <CardActions className={classes.actions} align="center"> + <NextLink href="/auth/register" passHref> + <Button size="small" id="SignInRegister"> + {t('signin.register')} + </Button> + </NextLink> </CardActions> </form> );

@@ -115,10 +134,16 @@ const useStyles = makeStyles(theme => ({

content: { display: 'flex', flexDirection: 'column', + padding: theme.spacing(0, 6), }, actions: { justifyContent: 'center', marginBottom: theme.spacing(2), + }, + divider: { + width: '100%', + textAlign: 'center', + margin: theme.spacing(2, 0), }, })); export default SignIn;
D frontend/containers/SignUpForm/index.tsx

@@ -1,175 +0,0 @@

-import {useState, useMemo} from 'react'; -import {useTranslation} from 'react-i18next'; -import TextField from '@material-ui/core/TextField'; -import {useRouter} from 'next/router'; -import Button from '@material-ui/core/Button'; -import CardContent from '@material-ui/core/CardContent'; -import CardActions from '@material-ui/core/CardActions'; -import Typography from '@material-ui/core/Typography'; -import CircularProgress from '@material-ui/core/CircularProgress'; -import {makeStyles} from '@material-ui/core/styles'; -import useToastsStore from '../../stores/useToastStore'; -import {useRegisterMutation} from '../../generated/graphql'; -import Link from 'next/link'; - -const SignUp = () => { - const {t, i18n} = useTranslation(); - const classes = useStyles(); - const addToast = useToastsStore(s => s.addToast); - const router = useRouter(); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(''); - const [firstName, setFirstName] = useState(''); - const [lastName, setLastName] = useState(''); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [register] = useRegisterMutation(); - - const canSubmit = useMemo( - () => - [firstName, lastName, email, password].filter(s => s.length < 2) - .length === 0, - [firstName, lastName, email, password] - ); - - const onSubmit = async e => { - e.preventDefault?.(); - if (isLoading) return; - setIsLoading(true); - try { - const lang = i18n.language.toUpperCase(); - await register({ - variables: { - user: { - username: email, - email, - password, - firstName, - lastName, - lang, - }, - }, - }); - router.push('/auth/confirm'); - } catch (error) { - const strapiError = error.message; - console.error({strapiError}); - if (strapiError === 'Email or Username are already taken') - setError(t('signup.errors.email_taken')); - else addToast(t(`generic.errors.unknown`)); - } - setIsLoading(false); - return false; - }; - - return ( - <form onSubmit={onSubmit}> - <CardContent className={classes.content}> - <Typography variant="h6">{t('signup.title')}</Typography> - <TextField - label={t('signup.firstName')} - fullWidth - autoFocus - margin="dense" - value={firstName} - required={true} - onChange={({target: {value = ''}}) => setFirstName(value)} - id="SignUpFirstName" - name="firstName" - /> - <TextField - label={t('signup.lastName')} - fullWidth - required={true} - margin="dense" - value={lastName} - onChange={({target: {value = ''}}) => setLastName(value)} - id="SignUpLastName" - name="lastName" - /> - <TextField - label={t('signup.email')} - fullWidth - required={true} - error={!!error} - helperText={error} - margin="dense" - value={email} - onChange={({target: {value = ''}}) => setEmail(value)} - id="SignUpEmail" - name="email" - type="email" - /> - <TextField - label={t('signup.password')} - fullWidth - required={true} - margin="dense" - value={password} - onChange={({target: {value = ''}}) => setPassword(value)} - id="SignUpEmail" - name="password" - type="password" - /> - </CardContent> - <CardActions className={classes.actions}> - <Link href="/auth/login" passHref> - <Button id="SignUpLogin" variant="text"> - {t('signup.login')} - </Button> - </Link> - <Button - color="primary" - variant="contained" - type="submit" - disabled={!canSubmit} - className={classes.button} - aria-disabled={!canSubmit} - id="SignUpSubmit" - endIcon={ - isLoading && ( - <CircularProgress - className={classes.loader} - size={20} - color="secondary" - /> - ) - } - > - {t('signup.submit')} - </Button> - </CardActions> - </form> - ); -}; - -const getStrapiError = error => { - try { - if (error.message?.[0]?.messages?.[0]) - return error.message[0].messages[0].id; - return error?.graphQLErrors?.[0].extensions.exception.data.message?.[0] - ?.messages[0].id; - } catch { - return 'generic.error'; - } -}; - -const useStyles = makeStyles(theme => ({ - content: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - }, - loader: { - marginLeft: '14px', - color: theme.palette.background.paper, - }, - actions: { - justifyContent: 'center', - marginBottom: theme.spacing(2), - }, - button: { - margin: theme.spacing(1), - }, -})); -export default SignUp;
M frontend/generated/graphql.tsxfrontend/generated/graphql.tsx

@@ -170,6 +170,7 @@ date?: Maybe<Scalars['Date']>;

description?: Maybe<Scalars['String']>; email: Scalars['String']; name: Scalars['String']; + passengers?: Maybe<PassengerRelationResponseCollection>; position?: Maybe<Scalars['JSON']>; travels?: Maybe<TravelRelationResponseCollection>; updatedAt?: Maybe<Scalars['DateTime']>;

@@ -178,15 +179,15 @@ waitingPassengers?: Maybe<PassengerRelationResponseCollection>;

}; -export type EventTravelsArgs = { - filters?: InputMaybe<TravelFiltersInput>; +export type EventPassengersArgs = { + filters?: InputMaybe<PassengerFiltersInput>; pagination?: InputMaybe<PaginationArg>; sort?: InputMaybe<Array<InputMaybe<Scalars['String']>>>; }; -export type EventWaitingPassengersArgs = { - filters?: InputMaybe<PassengerFiltersInput>; +export type EventTravelsArgs = { + filters?: InputMaybe<TravelFiltersInput>; pagination?: InputMaybe<PaginationArg>; sort?: InputMaybe<Array<InputMaybe<Scalars['String']>>>; };

@@ -214,12 +215,12 @@ name?: InputMaybe<StringFilterInput>;

newsletter?: InputMaybe<BooleanFilterInput>; not?: InputMaybe<EventFiltersInput>; or?: InputMaybe<Array<InputMaybe<EventFiltersInput>>>; + passengers?: InputMaybe<PassengerFiltersInput>; position?: InputMaybe<JsonFilterInput>; travels?: InputMaybe<TravelFiltersInput>; updatedAt?: InputMaybe<DateTimeFilterInput>; users?: InputMaybe<UsersPermissionsUserFiltersInput>; uuid?: InputMaybe<StringFilterInput>; - waitingPassengers?: InputMaybe<PassengerFiltersInput>; }; export type EventInput = {

@@ -229,11 +230,11 @@ description?: InputMaybe<Scalars['String']>;

email?: InputMaybe<Scalars['String']>; name?: InputMaybe<Scalars['String']>; newsletter?: InputMaybe<Scalars['Boolean']>; + passengers?: InputMaybe<Array<InputMaybe<Scalars['ID']>>>; position?: InputMaybe<Scalars['JSON']>; travels?: InputMaybe<Array<InputMaybe<Scalars['ID']>>>; users?: InputMaybe<Array<InputMaybe<Scalars['ID']>>>; uuid?: InputMaybe<Scalars['String']>; - waitingPassengers?: InputMaybe<Array<InputMaybe<Scalars['ID']>>>; }; export type EventRelationResponseCollection = {

@@ -1309,6 +1310,7 @@ email: Scalars['String'];

firstName?: InputMaybe<Scalars['String']>; lang?: InputMaybe<Scalars['String']>; lastName?: InputMaybe<Scalars['String']>; + newsletterConsent?: InputMaybe<Scalars['Boolean']>; password: Scalars['String']; username: Scalars['String']; };

@@ -1437,6 +1439,7 @@ firstName?: InputMaybe<StringFilterInput>;

id?: InputMaybe<IdFilterInput>; lang?: InputMaybe<StringFilterInput>; lastName?: InputMaybe<StringFilterInput>; + newsletterConsent?: InputMaybe<BooleanFilterInput>; not?: InputMaybe<UsersPermissionsUserFiltersInput>; onboardingCreator?: InputMaybe<BooleanFilterInput>; onboardingUser?: InputMaybe<BooleanFilterInput>;

@@ -1460,6 +1463,7 @@ events?: InputMaybe<Array<InputMaybe<Scalars['ID']>>>;

firstName?: InputMaybe<Scalars['String']>; lang?: InputMaybe<Enum_Userspermissionsuser_Lang>; lastName?: InputMaybe<Scalars['String']>; + newsletterConsent?: InputMaybe<Scalars['Boolean']>; oldPassword?: InputMaybe<Scalars['String']>; onboardingCreator?: InputMaybe<Scalars['Boolean']>; onboardingUser?: InputMaybe<Scalars['Boolean']>;
A frontend/layouts/ConfirmLayout.tsx

@@ -0,0 +1,32 @@

+import Card from '@material-ui/core/Card'; +import CardMedia from '@material-ui/core/CardMedia'; +import CardContent from '@material-ui/core/CardContent'; +import {makeStyles} from '@material-ui/core/styles'; +import Layout from './Centered'; +import Logo from '../components/Logo'; + +const CommonConfirm = ({children}) => { + const {wrapper} = useStyles(); + + return ( + <Layout displayMenu={false}> + <Card> + <CardMedia component={Logo} /> + <CardContent className={wrapper}> + {children} + </CardContent> + </Card> + </Layout> + ); +}; + +const useStyles = makeStyles(theme => ({ + wrapper: { + padding: theme.spacing(0, 8 ), + '&:last-child': { + paddingBottom: theme.spacing(12), + }, + }, +})); + +export default CommonConfirm;
M frontend/locales/en.jsonfrontend/locales/en.json

@@ -127,7 +127,7 @@ "travel.passengers.add": "Add a passenger",

"travel.passengers.add_to_travel": "Add a passenger", "travel.passengers.add_to_waitingList": "Add someone", "travel.passengers.add_to_car": "Add to car", - "travel.passengers.add_me": "Add me", + "travel.passengers.add_me": "Add myself", "travel.passengers.register_to_waiting_list": "Register to waiting list", "travel.passengers.add_someone": "Add someone", "travel.passengers.location": "Meeting place",

@@ -199,16 +199,23 @@ "passenger.input.email_helper": "Optional - Get notified if cars are added",

"passenger.input.email_helper_car": "Optional", "passenger.deleted": "The passenger has been deleted from the event.", "signup.title": "Sign up", + "signup.create": "Create an account", + "signup.conditions": "By creating an account, you agree to [the terms of Caroster](https://caroster.io/en/terms)", + "signup.createForm": "Create an account\ninformation to fullfill", + "signup.with_mail": "Continue with an email", "signup.email": "Email", "signup.firstName": "First name", "signup.lastName": "Last name", "signup.password": "Password", + "signup.newsletter.consent": "I am interested in car pooling, I want to subscribe to the newsletter.", "signup.submit": "Create your account", + "signup.account_already": "Do you already have an account ?", "signup.login": "$t(menu.login)", "signup.errors.email_taken": "This email is already associated with an account", - "confirm.title": "Confirm your account", + "confirm.title": "Confirm your email", "confirm.text": "You have received an email with a link. Please click on this link to confirm your account.", "confirm.login": "Return to the login screen", + "confirm.google.title": "Complete registration", "signin.title": "Sign in", "signin.email": "Email", "signin.password": "Password",

@@ -217,7 +224,9 @@ "signin.register": "$t(menu.register)",

"signin.errors.CredentialsSignin": "Check your email and password", "signin.errors.EmailNotConfirmed": "Your account has not been confirmed. Please check your emails", "signin.errors.OAuthCallback": "Impossible to use Google authentication currently", + "signin.or": "OR", "signin.withGoogle": "Use a Google account", + "signin.no_account": "You don't have an account ?", "lost_password.title": "Password recovery", "lost_password.reset_title": "Definition of a new password", "lost_password.message": "Lost your password?",
M frontend/locales/fr.jsonfrontend/locales/fr.json

@@ -199,17 +199,24 @@ "passenger.input.email_helper": "Optionnel - Soyez notifié si des voitures sont ajoutées",

"passenger.input.email_helper_car": "Optionnel", "passenger.deleted": "Le passager a été supprimé de l'événement.", "signup.title": "Inscription", + "signup.create": "Créer un compte", + "signup.conditions": "En créant un compte, vous acceptez [les conditions d’utilisations](https://caroster.io/fr/conditions-utilisation) de Caroster", + "signup.createForm": "Créer un compte\ninformations à remplir", + "signup.with_mail": "Continuer avec un email", "signup.email": "Email", "signup.firstName": "Prénom", "signup.lastName": "Nom", "signup.password": "Mot de passe", + "signup.newsletter.consent": "Le covoiturage m'intéresse, je souhaite recevoir des nouvelles de Caroster", "signup.submit": "Créer son compte", + "signup.account_already": "Vous avez déjà un compte ?", "signup.login": "$t(menu.login)", "signup.errors.email_taken": "Cet email est déjà associé à un compte", - "confirm.title": "Confirmez votre compte", + "confirm.title": "Confirmez votre email", "confirm.text": "Vous avez reçu un email avec un lien. Merci de cliquer sur ce lien pour confirmer votre compte.", "confirm.login": "Retour à l'écran de connexion", - "signin.title": "Connexion", + "confirm.google.title": "Finaliser l'inscription", + "signin.title": "Se connecter", "signin.email": "Email", "signin.password": "Mot de passe", "signin.login": "$t(menu.login)",

@@ -217,10 +224,12 @@ "signin.register": "$t(menu.register)",

"signin.errors.CredentialsSignin": "Vérifier votre email et mot de passe", "signin.errors.EmailNotConfirmed": "Votre compte n'a pas été confirmé. Merci de vérifier vos emails", "signin.errors.OAuthCallback": "Impossible d'utiliser la connexion avec un compte Google actuellement", - "signin.withGoogle": "Utiliser un compte Google", + "signin.or": "OU", + "signin.withGoogle": "Continuer avec Google", + "signin.no_account": "Vous n'avez pas de compte ?", "lost_password.title": "Récupération de mot de passe", "lost_password.reset_title": "Définition d'un nouveau mot de passe", - "lost_password.message": "Perdu son mot de passe ?", + "lost_password.message": "Mot de passe oublié ?", "lost_password.email": "Votre email", "lost_password.password": "Nouveau mot de passe", "lost_password.password_confirmation": "Confirmation du nouveau mot de passe",
M frontend/pages/api/nauth/[...nextauth].jsfrontend/pages/api/nauth/[...nextauth].js

@@ -53,6 +53,8 @@ token.jwt = data.jwt;

token.email = data.user.email; token.username = data.user.firstname; token.lang = data.user.lang?.toLowerCase(); + token.provider = account.provider; + token.userCreatedAt = data.user.createdAt; } // Strapi Auth

@@ -62,6 +64,8 @@ token.jwt = user.jwt;

token.email = user.email; token.username = user.firstname; token.lang = user.lang?.toLowerCase(); + token.provider = account.provider; + token.userCreatedAt = user.createdAt; } return token;
D frontend/pages/auth/confirm.tsx

@@ -1,46 +0,0 @@

-import Card from '@material-ui/core/Card'; -import CardMedia from '@material-ui/core/CardMedia'; -import Button from '@material-ui/core/Button'; -import CardContent from '@material-ui/core/CardContent'; -import CardActionArea from '@material-ui/core/CardActions'; -import CardActions from '@material-ui/core/CardActions'; -import Typography from '@material-ui/core/Typography'; -import {useTranslation} from 'react-i18next'; -import Layout from '../../layouts/Centered'; -import Logo from '../../components/Logo'; -import Link from 'next/link'; - -const Confirm = () => { - const {t} = useTranslation(); - - return ( - <Layout displayMenu={false}> - <Card> - <CardMedia component={Logo} /> - <CardContent> - <Typography gutterBottom variant="h5" component="h2"> - {t('confirm.title')} - </Typography> - <Typography variant="body2" color="textSecondary" component="p"> - {t('confirm.text')} - </Typography> - </CardContent> - <CardActionArea> - <CardActions> - <Link href="/auth/login" passHref> - <Button - color="primary" - variant="contained" - id="SignUpSuccessLogin" - > - {t('confirm.login')} - </Button> - </Link> - </CardActions> - </CardActionArea> - </Card> - </Layout> - ); -}; - -export default Confirm;
A frontend/pages/auth/confirm/google.tsx

@@ -0,0 +1,81 @@

+import Typography from '@material-ui/core/Typography'; +import Icon from '@material-ui/core/Icon'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import Checkbox from '@material-ui/core/Checkbox'; +import Button from '@material-ui/core/Button'; +import Box from '@material-ui/core/Box'; +import {makeStyles} from '@material-ui/core/styles'; +import {useTranslation} from 'react-i18next'; +import {useState} from 'react'; +import pageUtils from '../../../lib/pageUtils'; +import CommonConfirm from '../../../layouts/ConfirmLayout'; +import {useUpdateMeMutation} from '../../../generated/graphql'; +import useRedirectUrlStore from '../../../stores/useRedirectUrl'; +import router from 'next/router'; + +const Confirm = () => { + const {t} = useTranslation(); + const classes = useStyles(); + const [newsletterConsent, setNewsletterConsent] = useState(false); + const [updateMe] = useUpdateMeMutation(); + const getRedirectUrl = useRedirectUrlStore(s => s.getRedirectUrl); + const onSubmit = async () => { + await updateMe({variables: {userUpdate: {newsletterConsent}}}); + const callbackUrl = getRedirectUrl() || '/'; + router.push(callbackUrl); + }; + + return ( + <CommonConfirm> + <Typography variant="overline" component="h5" align="center"> + {t('signup.create')} + </Typography> + <Typography variant="h5" component="h2" align="center"> + {t('confirm.google.title')} + </Typography> + <Typography align="center" className={classes.margins} component="div"> + <Icon fontSize="large">mail</Icon> + </Typography> + <FormControlLabel + className={classes.newsletter} + control={ + <Checkbox + className={classes.checkbox} + color="primary" + value={newsletterConsent} + onChange={({target: {checked = false}}) => + setNewsletterConsent(checked) + } + /> + } + label={t('signup.newsletter.consent')} + /> + <Box className={classes.center}> + <Button variant="contained" color="secondary" onClick={onSubmit}> + {t('generic.confirm')} + </Button> + </Box> + </CommonConfirm> + ); +}; + +const useStyles = makeStyles(theme => ({ + margins: { + margin: theme.spacing(5, 0), + }, + newsletter: { + width: '100%', + margin: theme.spacing(2, 0), + }, + checkbox: { + padding: 0, + marginRight: theme.spacing(2), + }, + center: { + textAlign: 'center', + }, +})); + +export default Confirm; + +export const getServerSideProps = pageUtils.getServerSideProps();
A frontend/pages/auth/confirm/index.tsx

@@ -0,0 +1,48 @@

+import Typography from '@material-ui/core/Typography'; +import Icon from '@material-ui/core/Icon'; +import {makeStyles} from '@material-ui/core/styles'; +import {useTranslation} from 'react-i18next'; +import CommonConfirm from '../../../layouts/ConfirmLayout'; +import pageUtils from '../../../lib/pageUtils'; + +const Confirm = () => { + const {t} = useTranslation(); + const {margins} = useStyles(); + + return ( + <CommonConfirm> + <Typography variant="overline" component="h5" align="center"> + {t('confirm.title')} + </Typography> + <Typography + variant="h5" + component="h2" + align="center" + > + {t('confirm.title')} + </Typography> + <Typography align="center" className={margins} component="div"> + <Icon fontSize="large">mail</Icon> + </Typography> + <Typography + className={margins} + variant="body2" + color="textSecondary" + component="p" + align="center" + > + {t('confirm.text')} + </Typography> + </CommonConfirm> + ); +}; + +const useStyles = makeStyles(theme => ({ + margins: { + margin: theme.spacing(5, 0), + }, +})); + +export default Confirm; + +export const getServerSideProps = pageUtils.getServerSideProps();
M frontend/pages/auth/login.tsxfrontend/pages/auth/login.tsx

@@ -5,7 +5,6 @@ import {useTranslation} from 'react-i18next';

import Layout from '../../layouts/Centered'; import Logo from '../../components/Logo'; import SignInForm from '../../containers/SignInForm'; -import LoginGoogle from '../../containers/LoginGoogle'; import LanguagesIcon from '../../containers/Languages/Icon'; import {getSession} from 'next-auth/react'; import pageUtils from '../../lib/pageUtils';

@@ -22,8 +21,6 @@ <Layout menuTitle={t('signin.title')} displayMenu={false}>

<Card> <CardMedia component={Logo} /> <SignInForm error={props?.error} /> - <Divider /> - <LoginGoogle /> </Card> <LanguagesIcon /> </Layout>
D frontend/pages/auth/register.tsx

@@ -1,26 +0,0 @@

-import Card from '@material-ui/core/Card'; -import CardMedia from '@material-ui/core/CardMedia'; -import Divider from '@material-ui/core/Divider'; -import {useTranslation} from 'react-i18next'; -import Layout from '../../layouts/Centered'; -import SignUpForm from '../../containers/SignUpForm'; -import LoginGoogle from '../../containers/LoginGoogle'; -import Logo from '../../components/Logo'; -import LanguagesIcon from '../../containers/Languages/Icon'; - -const SignUp = () => { - const {t} = useTranslation(); - return ( - <Layout menuTitle={t('signup.title')} displayMenu={false}> - <Card> - <CardMedia component={Logo} /> - <SignUpForm /> - <Divider /> - <LoginGoogle /> - <LanguagesIcon /> - </Card> - </Layout> - ); -}; - -export default SignUp;
A frontend/pages/auth/register/index.tsx

@@ -0,0 +1,88 @@

+import Card from '@material-ui/core/Card'; +import CardMedia from '@material-ui/core/CardMedia'; +import {useTranslation} from 'react-i18next'; +import Layout from '../../../layouts/Centered'; +import Logo from '../../../components/Logo'; +import LanguagesIcon from '../../../containers/Languages/Icon'; +import CardContent from '@material-ui/core/CardContent'; +import SignUpActions from '../../../containers/MailSignUpForm/SignupActions'; +import Typography from '@material-ui/core/Typography'; +import {makeStyles} from '@material-ui/core/styles'; +import Box from '@material-ui/core/Box'; +import Divider from '@material-ui/core/Divider'; +import Button from '@material-ui/core/Button'; +import LoginGoogle from '../../../containers/LoginGoogle'; +import Link from 'next/link'; +import Markdown from '../../../components/Markdown'; + +const MailSignup = () => { + const {t} = useTranslation(); + const classes = useStyles(); + + return ( + <Layout menuTitle={t('signup.title')} displayMenu={false}> + <Card> + <CardMedia component={Logo} /> + <CardContent className={classes.content}> + <Typography variant="overline" component="h5" align="center"> + {t('signup.create')} + </Typography> + <Box className={classes.content}> + <Link href="/auth/register/mail" passHref> + <Button + color="primary" + variant="contained" + fullWidth + className={classes.button} + > + {t('signup.with_mail')} + </Button> + </Link> + <LoginGoogle /> + </Box> + <Box className={classes.divider}> + <Markdown + className={classes.conditions} + variant="overline" + align="center" + > + {t('signup.conditions')} + </Markdown> + + <Divider /> + </Box> + <Typography align="center" variant="body2"> + {t('signup.account_already')} + </Typography> + </CardContent> + <SignUpActions /> + <LanguagesIcon /> + </Card> + </Layout> + ); +}; + +const useStyles = makeStyles(theme => ({ + content: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + width: '100%', + padding: theme.spacing(0, 6), + }, + button: { + margin: theme.spacing(8, 1, 4, 1), + }, + divider: { + width: '100%', + textAlign: 'center', + margin: theme.spacing(10, 0, 2, 0), + }, + conditions: { + '& a': { + color: 'inherit', + }, + }, +})); + +export default MailSignup;
A frontend/pages/auth/register/mail.tsx

@@ -0,0 +1,22 @@

+import Card from '@material-ui/core/Card'; +import CardMedia from '@material-ui/core/CardMedia'; +import {useTranslation} from 'react-i18next'; +import Layout from '../../../layouts/Centered'; +import MailSignUpForm from '../../../containers/MailSignUpForm'; +import Logo from '../../../components/Logo'; +import LanguagesIcon from '../../../containers/Languages/Icon'; + +const MailSignup = () => { + const {t} = useTranslation(); + return ( + <Layout menuTitle={t('signup.title')} displayMenu={false}> + <Card> + <CardMedia component={Logo} /> + <MailSignUpForm /> + <LanguagesIcon /> + </Card> + </Layout> + ); +}; + +export default MailSignup;
M frontend/pages/index.tsxfrontend/pages/index.tsx

@@ -1,11 +1,12 @@

import {useRouter} from 'next/router'; import {useTranslation} from 'react-i18next'; +import moment from 'moment'; import Layout from '../layouts/Centered'; import CreateEvent from '../containers/CreateEvent'; import LanguagesIcon from '../containers/Languages/Icon'; import Paper from '../components/Paper'; import Logo from '../components/Logo'; -import {useSession} from 'next-auth/react'; +import {getSession, useSession} from 'next-auth/react'; import pageUtils from '../lib/pageUtils'; import {useEffect} from 'react'; import useRedirectUrlStore from '../stores/useRedirectUrl';

@@ -74,6 +75,21 @@ </Layout>

); }; -export const getServerSideProps = pageUtils.getServerSideProps(); +export const getServerSideProps = async (context: any) => { + const session = await getSession(context); + const {provider, userCreatedAt} = session?.token || {}; + const isFirstLogin = userCreatedAt + ? moment().subtract({seconds: 3}).isBefore(userCreatedAt) + : false; + if (provider === 'google' && isFirstLogin) + return { + redirect: { + destination: '/auth/confirm/google', + permanent: false, + }, + }; + + return pageUtils.getServerSideProps()(context); +}; export default Home;