Skip to content

Commit

Permalink
Merge pull request #2278 from ever-co/feat/login-with-password
Browse files Browse the repository at this point in the history
Login with Password
  • Loading branch information
evereq authored Mar 8, 2024
2 parents cd20560 + dbc5fa1 commit 5f3eb17
Show file tree
Hide file tree
Showing 27 changed files with 663 additions and 426 deletions.
90 changes: 62 additions & 28 deletions apps/web/app/[locale]/auth/passcode/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

import { getAccessTokenCookie, getActiveUserIdCookie } from '@app/helpers';
import { TAuthenticationPasscode, useAuthenticationPasscode } from '@app/hooks';
import { IClassName } from '@app/interfaces';
import { IClassName, ISigninEmailConfirmWorkspaces } from '@app/interfaces';
import { clsxm } from '@app/utils';
import { AuthCodeInputField, Avatar, BackButton, Button, Card, InputField, SpinnerLoader, Text } from 'lib/components';
import { CircleIcon, CheckCircleOutlineIcon } from 'assets/svg';
import { AuthLayout } from 'lib/layout';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { FormEvent, useCallback, useEffect, useRef, useState } from 'react';
import { Dispatch, FormEvent, FormEventHandler, SetStateAction, useCallback, useEffect, useRef, useState } from 'react';

import stc from 'string-to-color';

Expand Down Expand Up @@ -98,11 +98,17 @@ function EmailScreen({ form, className }: { form: TAuthenticationPasscode } & IC
/>

<div className="flex items-center justify-between w-full mt-6">
{/* Send code */}
<div className="flex flex-col items-start gap-2">
<div className="flex items-center justify-start gap-2 text-sm">
<span className="text-sm">{t('pages.authLogin.HAVE_PASSWORD')}</span>
<Link href="/auth/password" className="text-primary dark:text-primary-light">
{t('pages.authLogin.LOGIN_WITH_PASSWORD')}.
</Link>
</div>

<div className="flex items-center justify-start gap-2 text-sm">
<span>{t('common.DONT_HAVE_ACCOUNT')}</span>
<Link href="/auth/team" className="text-primary">
<Link href="/auth/team" className="text-primary dark:text-primary-light">
<span>{t('common.REGISTER')}</span>
</Link>
</div>
Expand Down Expand Up @@ -227,8 +233,6 @@ function PasscodeScreen({ form, className }: { form: TAuthenticationPasscode } &
}

function WorkSpaceScreen({ form, className }: { form: TAuthenticationPasscode } & IClassName) {
const t = useTranslations();

const [selectedWorkspace, setSelectedWorkspace] = useState<number>(0);
const [selectedTeam, setSelectedTeam] = useState('');
const router = useRouter();
Expand All @@ -252,6 +256,7 @@ function WorkSpaceScreen({ form, className }: { form: TAuthenticationPasscode }
if (form.workspaces.length === 1 && currentTeams?.length === 1) {
setSelectedTeam(currentTeams[0].team_id);
}

if (form.workspaces.length === 1 && (currentTeams?.length || 0) <= 1) {
setTimeout(() => {
document.getElementById('continue-to-workspace')?.click();
Expand All @@ -268,12 +273,43 @@ function WorkSpaceScreen({ form, className }: { form: TAuthenticationPasscode }
}
}, [form.authScreen, router]);

console.log(form);
return (
<WorkSpaceComponent
className={className}
workspaces={form.workspaces}
onSubmit={signInToWorkspace}
onBackButtonClick={() => {
form.authScreen.setScreen('email');
form.setErrors({});
}}
selectedWorkspace={selectedWorkspace}
setSelectedWorkspace={setSelectedWorkspace}
setSelectedTeam={setSelectedTeam}
selectedTeam={selectedTeam}
signInWorkspaceLoading={form.signInWorkspaceLoading}
/>
);
}

type IWorkSpace = {
className?: string;
workspaces: ISigninEmailConfirmWorkspaces[];
onSubmit?: FormEventHandler<HTMLFormElement>;
onBackButtonClick?: () => void;
selectedWorkspace: number;
setSelectedWorkspace: Dispatch<SetStateAction<number>>;
signInWorkspaceLoading?: boolean;
setSelectedTeam: Dispatch<SetStateAction<string>>;
selectedTeam: string;
};

export function WorkSpaceComponent(props: IWorkSpace) {
const t = useTranslations();

return (
<form
className={clsxm(className, 'flex justify-center w-full')}
onSubmit={signInToWorkspace}
className={clsxm(props.className, 'flex justify-center w-full')}
onSubmit={props.onSubmit}
autoComplete="off"
>
<Card className="w-full max-w-[30rem] dark:bg-[#25272D]" shadow="custom">
Expand All @@ -283,11 +319,11 @@ function WorkSpaceScreen({ form, className }: { form: TAuthenticationPasscode }
</Text.Heading>

<div className="flex flex-col w-full gap-4 max-h-[16.9375rem] overflow-scroll scrollbar-hide">
{form.workspaces?.map((worksace, index) => (
{props.workspaces?.map((worksace, index) => (
<div
key={index}
className={`w-full flex flex-col border border-[#0000001A] dark:border-[#34353D] ${
selectedWorkspace === index ? 'bg-[#FCFCFC] dark:bg-[#1F2024]' : ''
props.selectedWorkspace === index ? 'bg-[#FCFCFC] dark:bg-[#1F2024]' : ''
} hover:bg-[#FCFCFC] dark:hover:bg-[#1F2024] rounded-xl`}
>
<div className="text-base font-medium py-[1.25rem] px-4 flex flex-col gap-[1.0625rem]">
Expand All @@ -296,18 +332,18 @@ function WorkSpaceScreen({ form, className }: { form: TAuthenticationPasscode }
<span
className="hover:cursor-pointer"
onClick={() => {
setSelectedWorkspace(index);
props.setSelectedWorkspace(index);
if (
selectedTeam &&
props.selectedTeam &&
!worksace.current_teams
?.map((team) => team.team_id)
.includes(selectedTeam)
.includes(props.selectedTeam)
) {
setSelectedTeam(worksace.current_teams[0].team_id);
props.setSelectedTeam(worksace.current_teams[0].team_id);
}
}}
>
{selectedWorkspace === index ? (
{props.selectedWorkspace === index ? (
<CheckCircleOutlineIcon className="w-6 h-6 stroke-[#27AE60] fill-[#27AE60]" />
) : (
<CircleIcon className="w-6 h-6" />
Expand Down Expand Up @@ -338,13 +374,13 @@ function WorkSpaceScreen({ form, className }: { form: TAuthenticationPasscode }
<span
className="hover:cursor-pointer"
onClick={() => {
setSelectedTeam(team.team_id);
if (selectedWorkspace !== index) {
setSelectedWorkspace(index);
props.setSelectedTeam(team.team_id);
if (props.selectedWorkspace !== index) {
props.setSelectedWorkspace(index);
}
}}
>
{selectedTeam === team.team_id ? (
{props.selectedTeam === team.team_id ? (
<CheckCircleOutlineIcon className="w-5 h-5 stroke-[#27AE60] fill-[#27AE60]" />
) : (
<CircleIcon className="w-5 h-5" />
Expand All @@ -361,19 +397,17 @@ function WorkSpaceScreen({ form, className }: { form: TAuthenticationPasscode }
<div className="flex items-center justify-between w-full">
<div className="flex flex-col space-y-2">
<div>
<BackButton
onClick={() => {
form.authScreen.setScreen('email');
form.setErrors({});
}}
/>
<BackButton onClick={props.onBackButtonClick} />
</div>
</div>

<Button
type="submit"
loading={form.signInWorkspaceLoading}
disabled={form.signInWorkspaceLoading || (!selectedWorkspace && selectedWorkspace !== 0)}
loading={props.signInWorkspaceLoading}
disabled={
props.signInWorkspaceLoading ||
(!props.selectedWorkspace && props.selectedWorkspace !== 0)
}
id="continue-to-workspace"
>
{t('common.CONTINUE')}
Expand Down
153 changes: 153 additions & 0 deletions apps/web/app/[locale]/auth/password/component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
'use client';

import { getAccessTokenCookie } from '@app/helpers';
import { TAuthenticationPassword, useAuthenticationPassword } from '@app/hooks';
import { IClassName } from '@app/interfaces';
import { clsxm } from '@app/utils';
import { Button, Card, InputField, Text } from 'lib/components';
import { AuthLayout } from 'lib/layout';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useCallback, useEffect, useState } from 'react';
import { WorkSpaceComponent } from '../passcode/component';

export default function AuthPassword() {
const t = useTranslations();
const form = useAuthenticationPassword();

return (
<AuthLayout
title={t('pages.authLogin.HEADING_TITLE')}
description={t('pages.authPassword.HEADING_DESCRIPTION')}
>
<div className="w-[98%] md:w-[550px] overflow-x-hidden">
<div className={clsxm('flex flex-row transition-[transform] duration-500')}>
{form.authScreen.screen === 'login' && <LoginForm form={form} />}

{form.authScreen.screen === 'workspace' && <WorkSpaceScreen form={form} className="w-full" />}
</div>
</div>
</AuthLayout>
);
}

function LoginForm({ form }: { form: TAuthenticationPassword }) {
const t = useTranslations();

return (
<Card className={clsxm('w-full dark:bg-[#25272D]')} shadow="bigger">
<form onSubmit={form.handleSubmit} className="flex flex-col items-center justify-between h-full w-full">
<Text.Heading as="h3" className="mb-10 text-center">
{t('pages.authLogin.LOGIN_WITH_PASSWORD')}
</Text.Heading>

<div className="w-full mb-8">
<InputField
name="email"
type="email"
placeholder={t('form.EMAIL_PLACEHOLDER')}
value={form.formValues.email}
errors={form.errors}
onChange={form.handleChange}
autoComplete="off"
wrapperClassName="dark:bg-[#25272D]"
className="dark:bg-[#25272D]"
/>

<InputField
type="password"
name="password"
placeholder={t('form.PASSWORD_PLACEHOLDER')}
className="dark:bg-[#25272D]"
wrapperClassName="mb-5 dark:bg-[#25272D]"
value={form.formValues.password}
errors={form.errors}
onChange={form.handleChange}
autoComplete="off"
/>
</div>

<div className="flex items-center justify-between w-full">
<div className="flex flex-col items-start gap-2">
<div className="flex items-center justify-start gap-2 text-sm">
<Link href="/auth/passcode" className="text-primary dark:text-primary-light">
{t('pages.authLogin.LOGIN_WITH_MAGIC_CODE')}.
</Link>
</div>

<div className="flex items-center justify-start gap-2 text-sm">
<span>{t('common.DONT_HAVE_ACCOUNT')}</span>
<Link href="/auth/team" className="text-primary dark:text-primary-light">
<span>{t('common.REGISTER')}</span>
</Link>
</div>
</div>

<Button type="submit" loading={form.signInLoading} disabled={form.signInLoading}>
{t('common.CONTINUE')}
</Button>
</div>
</form>
</Card>
);
}

function WorkSpaceScreen({ form, className }: { form: TAuthenticationPassword } & IClassName) {
const [selectedWorkspace, setSelectedWorkspace] = useState<number>(0);
const [selectedTeam, setSelectedTeam] = useState('');
const router = useRouter();

const signInToWorkspace = useCallback(
(e: any) => {
if (typeof selectedWorkspace !== 'undefined') {
form.handleWorkspaceSubmit(e, form.workspaces[selectedWorkspace].token, selectedTeam);
}
},
[selectedWorkspace, selectedTeam, form]
);

useEffect(() => {
if (form.workspaces.length === 1) {
setSelectedWorkspace(0);
}

const currentTeams = form.workspaces[0]?.current_teams;

if (form.workspaces.length === 1 && currentTeams?.length === 1) {
setSelectedTeam(currentTeams[0].team_id);
}

if (form.workspaces.length === 1 && (currentTeams?.length || 0) <= 1) {
setTimeout(() => {
document.getElementById('continue-to-workspace')?.click();
}, 100);
}
}, [form.workspaces]);

useEffect(() => {
if (form.authScreen.screen === 'workspace') {
const accessToken = getAccessTokenCookie();
if (accessToken && accessToken.length > 100) {
router.refresh();
}
}
}, [form.authScreen, router]);

return (
<WorkSpaceComponent
className={className}
workspaces={form.workspaces}
onSubmit={signInToWorkspace}
onBackButtonClick={() => {
form.authScreen.setScreen('login');
form.setErrors({});
}}
selectedWorkspace={selectedWorkspace}
setSelectedWorkspace={setSelectedWorkspace}
setSelectedTeam={setSelectedTeam}
selectedTeam={selectedTeam}
signInWorkspaceLoading={form.signInWorkspaceLoading}
/>
);
}
10 changes: 10 additions & 0 deletions apps/web/app/[locale]/auth/password/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { APPLICATION_DEFAULT_LANGUAGE } from '@app/constants';
import AuthPassword from './component';

export async function generateStaticParams() {
return [{ locale: APPLICATION_DEFAULT_LANGUAGE }];
}

export default function Page() {
return <AuthPassword />;
}
18 changes: 18 additions & 0 deletions apps/web/app/api/auth/signin-email-password/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { validateForm } from '@app/helpers/validations';
import { signInEmailPasswordRequest } from '@app/services/server/requests';

import { NextResponse } from 'next/server';

export async function POST(req: Request) {
const body = (await req.json()) as { email: string; password: string };

const { errors, isValid } = validateForm(['email', 'password'], body);

if (!isValid) {
return NextResponse.json({ errors }, { status: 400 });
}

const { data } = await signInEmailPasswordRequest(body.email, body.password);

return NextResponse.json(data);
}
1 change: 1 addition & 0 deletions apps/web/app/helpers/validations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type Ks = { [x: string]: string };

export const authFormValidate = (keys: (keyof IRegisterDataAPI)[], values: IRegisterDataAPI) => {
const err = {} as Err;

keys.forEach((key) => {
switch (key) {
case 'email':
Expand Down
Loading

0 comments on commit 5f3eb17

Please sign in to comment.