Skip to content

Commit

Permalink
Merge pull request #22 from uninus-opensource/feat/tourism-login-page
Browse files Browse the repository at this point in the history
Feat/tourism login page : Slicing login page for dashboard tourism
  • Loading branch information
maulanasdqn authored Mar 22, 2024
2 parents 13da195 + fb802bb commit 869f01b
Show file tree
Hide file tree
Showing 8 changed files with 286 additions and 5 deletions.
149 changes: 149 additions & 0 deletions apps/web/tourism/app/(auth)/auth/login/_modules/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
'use client';
import { Button } from '@psu/web-component-atoms';
import { Form } from '@psu/web-component-templates';
import { ControlledFieldText } from '@psu/web-component-organisms';
import { useForm } from 'react-hook-form';
import { TLoginRequest } from '@psu/entities';
import { FC, ReactElement, useEffect, useState } from 'react';
import z from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { signIn } from 'next-auth/react';
import { FaEye, FaEyeSlash } from 'react-icons/fa';
import Image from 'next/image';

const schema = z.object({
email: z.string().email({ message: 'Email tidak valid' }),
password: z
.string({ required_error: 'Kata sandi wajib diisi' })
.min(1, { message: 'Kata sandi wajib diisi' }),
});

export const AuthLoginModule: FC = (): ReactElement => {
const {
control,
handleSubmit,
formState: { errors, isValid },
} = useForm<TLoginRequest>({
resolver: zodResolver(schema),
mode: 'all',
defaultValues: {
email: '',
password: '',
},
});

const [error, setError] = useState<string | undefined>(undefined);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isVisible, setIsVisible] = useState<boolean>(false);

const router = useRouter();

const onSubmit = handleSubmit(async (data) => {
setIsLoading(true);
try {
const res = await signIn('credentials', {
email: data.email,
password: data.password,
redirect: false,
});

if (res?.ok) {
router.push('/dashboard');
}

if (res?.error) {
setError(res?.error);
console.log(res?.error);
}
} catch (err) {
console.log(err);
}
setIsLoading(false);
});

useEffect(() => {
setTimeout(() => {
setError(undefined);
}, 5000);
}, [error]);

const TogglePassword: FC = (): ReactElement => {
return (
<span
className="cursor-pointer"
onClick={() => {
setIsVisible(!isVisible);
}}
>
{isVisible ? <FaEye /> : <FaEyeSlash />}
</span>
);
};

return (
<section className="bg-white rounded-l-2xl p-2 md:p-6 min-h-screen flex flex-col justify-center items-center">
<div className="flex flex-col items-center gap-2 mb-10 lg:hidden">
<Image
src="/assets/logo.svg"
alt="logo"
width={75}
height={75}
priority
/>
<h1 className="text-2xl md:text-3xl font-bold text-black text-center">
WISATA DESA BOJONGSARI
</h1>
</div>
<Form
onSubmit={onSubmit}
className="shadow-none w-full sm:w-[50dvw] lg:w-[35dvw] border p-3 md:p-10 rounded-lg"
>
<h1 className="text-center text-3xl font-semibold">Login</h1>
{error && (
<span className="text-error bg-error-50 border-error border rounded-lg p-3">
{error}
</span>
)}
<section className="flex flex-col gap-y-3 mt-[18px]">
<ControlledFieldText
control={control}
name="email"
type="email"
size="md"
placeholder="Email"
status={errors.email ? 'error' : 'default'}
message={errors.email?.message}
/>
<ControlledFieldText
control={control}
name="password"
size="md"
type={isVisible ? 'text' : 'password'}
placeholder="Kata Sandi"
append={<TogglePassword />}
/>
</section>
<div className="w-full my-4 flex justify-start">
<Link
href="/auth/forgot"
className="font-semibold text-sm text-gray hover:text-black"
>
Lupa Kata Sandi?
</Link>
</div>
<div className="flex justify-center px-8">
<Button
disabled={isValid || isLoading}
type="submit"
size="lg"
className="w-full"
>
Masuk
</Button>
</div>
</Form>
</section>
);
};
78 changes: 78 additions & 0 deletions apps/web/tourism/app/(auth)/auth/login/_modules/login.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { PropsWithChildren } from 'react';
import { AuthLoginModule } from '.';
import { render } from '@testing-library/react';

jest.mock('react-avatar', () => ({
Avatar: jest.fn(() => <></>),
}));

jest.mock('next/image', () => ({
Image: jest.fn(() => <></>),
}));

jest.mock('react-hook-form', () => ({
...jest.requireActual('react-hook-form'),
Controller: () => <></>,
useFormContext: () => ({}),
useContext: () => ({}),
useForm: () => ({
control: {
register: jest.fn(),
unregister: jest.fn(),
getFieldState: jest.fn(),
_names: {
array: new Set('test'),
mount: new Set('test'),
unMount: new Set('test'),
watch: new Set('test'),
focus: 'test',
watchAll: false,
},
_subjects: {
watch: jest.fn(),
array: jest.fn(),
state: jest.fn(),
},
_getWatch: jest.fn(),
_formValues: ['test'],
_defaultValues: ['test'],
},
formState: {
errors: {
email: {
message: 'Email tidak valid',
},
password: {
message: 'Kata sandi wajib diisi',
},
},
},
handleSubmit: () => jest.fn(),
}),
}));

jest.mock('react');
jest.mock(
'next/link',
() =>
({ children }: PropsWithChildren) =>
children
);
jest.mock(
'react-select',
() =>
({ children }: PropsWithChildren) =>
children
);

jest.mock('next/navigation', () => ({
useRouter: jest.fn(),
usePathname: jest.fn(),
}));

describe('Auth Login Module', () => {
it('Should render successfully', () => {
const { baseElement } = render(<AuthLoginModule />);
expect(baseElement).toBeTruthy();
});
});
8 changes: 8 additions & 0 deletions apps/web/tourism/app/(auth)/auth/login/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { FC, ReactElement } from 'react';
import { BiLoaderAlt } from 'react-icons/bi';

const DashboardLoading: FC = (): ReactElement => {
return <BiLoaderAlt className="animate-spin" />;
};

export default DashboardLoading;
28 changes: 28 additions & 0 deletions apps/web/tourism/app/(auth)/auth/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { NextPage } from 'next';
import type { ReactElement } from 'react';
import { AuthLoginModule } from './_modules';
import Image from 'next/image';

const AuthLoginPage: NextPage = (): ReactElement => {
return (
<section className="flex overflow-y-hidden md:flex-row flex-col gap-y-5 lg:justify-between justify-center items-center w-full h-full min-h-screen">
<div className="hidden w-1/4 lg:flex flex-col shrink-0 justify-center item-center lg:items-center gap-y-4">
<Image
src="/assets/logo-desa.png"
alt="logo"
width={250}
height={219}
priority
/>
<h1 className="text-2xl md:text-3xl font-bold text-white text-center">
WISATA DESA BOJONGSARI
</h1>
</div>
<div className="md:w-3/4 w-full h-full md:pl-6">
<AuthLoginModule />
</div>
</section>
);
};

export default AuthLoginPage;
13 changes: 13 additions & 0 deletions apps/web/tourism/app/(auth)/template.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { FC, PropsWithChildren, ReactElement } from 'react';

const AuthTemplate: FC<Readonly<PropsWithChildren>> = (props): ReactElement => {
return (
<main className="flex overflow-hidden items-center w-full bg-white lg:bg-primary-50%">
<div className="z-10 w-full overflow-y-hidden h-full">
{props.children}
</div>
</main>
);
};

export default AuthTemplate;
Binary file added apps/web/tourism/public/assets/logo-desa.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 5 additions & 3 deletions libs/web/components/atoms/src/button/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ export const Button: FC<TButton> = ({
variantType = 'primary',
state = 'default',
useIconArrowDown,
className,
...props
}): ReactElement => {
const className = cn(
const classNames = cn(
'rounded-md text-white font-medium transition-all disabled:cursor-not-allowed disabled:hover:opacity-100 disabled:bg-primary-10% disabled:text-neutral-10% px-4 py-2',
className,
{
'bg-primary hover:bg-primary-60% active:bg-primary-90%':
variant === 'primary' && variantType === 'primary',
Expand Down Expand Up @@ -75,13 +77,13 @@ export const Button: FC<TButton> = ({

return match(props.href)
.with(undefined, () => (
<button className={className} {...props}>
<button className={classNames} {...props}>
{buttonState}
</button>
))
.with(P.string, (link) => (
<Link href={link}>
<button className={className} {...props}>
<button className={classNames} {...props}>
{buttonState}
</button>
</Link>
Expand Down
7 changes: 5 additions & 2 deletions libs/web/components/templates/src/form/form.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { TForm } from '@psu/entities';
import { cn, TForm } from '@psu/entities';
import { FC, ReactElement } from 'react';

export const Form: FC<TForm> = (props): ReactElement => {
return (
<form
className="w-full justify-center bg-white shadow-lg rounded-lg px-4 py-8 md:px-[48px] md:py-[32px] flex flex-col gap-y-3"
className={cn(
'w-full justify-center bg-white shadow-lg rounded-lg px-4 py-8 md:px-[48px] md:py-[32px] flex flex-col gap-y-3',
props.className
)}
{...props}
>
{props.children}
Expand Down

0 comments on commit 869f01b

Please sign in to comment.