-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[FE][CPF-65] Add employee - personal details #77
Changes from 12 commits
e774cfc
c65f844
6c12fb0
892b3e3
8917050
8e78081
1b26b45
3bad864
a41044f
2f006b4
5996a62
e6fdf0d
b8f3f36
6bf0d30
d16f9ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
# hooks start from the project root directory | ||
|
||
cd frontend | ||
|
||
yarn lint:fix && yarn format | ||
yarn lint:fix | ||
yarn format | ||
yarn compile |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { EmployeeSideStepper } from '@app/components/modules/EmployeeSideStepper'; | ||
import { WorkflowTopbar } from '@app/components/modules/WorkflowTopbar'; | ||
|
||
export default function PeopleLayout({ children }: Readonly<{ children: React.ReactNode }>) { | ||
return ( | ||
<div className="flex"> | ||
<div className="w-full"> | ||
<WorkflowTopbar /> | ||
<main className="p-8"> | ||
<div className="grid grid-cols-[minmax(200px,1fr),minmax(400px,1100px),1fr]"> | ||
<EmployeeSideStepper /> | ||
<div className="col-span-1 px-8">{children}</div> | ||
</div> | ||
</main> | ||
</div> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
'use client'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. when we set the convention of file and component structure/architecture I will get rid of that line of shame 😄 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As mentioned on the channel. Let's go for the interface files, hooks (logic), index and component that takes data from the hook There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I left that there because the whole page is a form and needs JS interactivity |
||
import { Button } from '@app/components/common/Button'; | ||
import { FormProvider } from '@app/components/common/FormProvider'; | ||
import { Input } from '@app/components/common/Input'; | ||
import { Typography } from '@app/components/common/Typography'; | ||
import { routes } from '@app/constants'; | ||
import { usePeopleStore } from '@app/store/peopleStore'; | ||
import { useEffect, useState } from 'react'; | ||
import { useForm } from 'react-hook-form'; | ||
|
||
enum PersonalDetailsFormNames { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have established to use consts instead of enums |
||
firstName = 'firstName', | ||
lastName = 'lastName', | ||
email = 'email', | ||
} | ||
interface PersonalDetailsForm { | ||
[PersonalDetailsFormNames.firstName]: string; | ||
[PersonalDetailsFormNames.lastName]: string; | ||
[PersonalDetailsFormNames.email]: string; | ||
} | ||
|
||
export default function PersonalDetails() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to our agreements about components structure, this should be moved to |
||
const form = useForm<PersonalDetailsForm>({ | ||
mode: 'onChange', | ||
defaultValues: { | ||
[PersonalDetailsFormNames.firstName]: '', | ||
[PersonalDetailsFormNames.lastName]: '', | ||
[PersonalDetailsFormNames.email]: '', | ||
}, | ||
}); | ||
const { isDirty, isValid } = form.formState; | ||
const [formValid, setFormValid] = useState(false); | ||
const updateProgress = usePeopleStore((state) => state.updateProgress); | ||
|
||
useEffect(() => { | ||
setFormValid(isDirty && isValid); | ||
}, [isDirty, isValid]); | ||
|
||
// INFO: update progress in sidebar stepper | ||
useEffect(() => { | ||
if (formValid) updateProgress({ [routes.people.addNew.personalDetails]: 'completed' }); | ||
else updateProgress({ [routes.people.addNew.personalDetails]: 'inProgress' }); | ||
}, [formValid, updateProgress]); | ||
|
||
return ( | ||
<FormProvider<PersonalDetailsForm> form={form}> | ||
<Typography variant="head-m/semibold" className="mb-6"> | ||
Personal details | ||
</Typography> | ||
<div className="mb-6 grid w-full grid-cols-2 gap-x-8 gap-y-6 rounded-[20px] border border-navy-200 bg-white p-8"> | ||
<Input | ||
name={PersonalDetailsFormNames.firstName} | ||
label="First name" | ||
options={{ | ||
minLength: { value: 2, message: 'First name must contain at least 2 characters' }, | ||
required: { value: true, message: 'First Name is required' }, | ||
}} | ||
/> | ||
<Input | ||
name={PersonalDetailsFormNames.lastName} | ||
label="Last name" | ||
options={{ | ||
minLength: { value: 2, message: 'Last name must contain at least 2 characters' }, | ||
required: { value: true, message: 'Last name is required' }, | ||
}} | ||
/> | ||
<Input | ||
name={PersonalDetailsFormNames.email} | ||
label="Email" | ||
options={{ | ||
pattern: { value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i, message: 'invalid email address' }, | ||
required: { value: true, message: 'Email is required' }, | ||
}} | ||
/> | ||
</div> | ||
<div className="flex justify-end"> | ||
<Button styleType="primary" variant="border" onClick={(e) => e.preventDefault()} disabled={!formValid}> | ||
Continue | ||
</Button> | ||
</div> | ||
</FormProvider> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { Sidebar } from '@app/components/modules/Sidebar'; | ||
import { Topbar } from '@app/components/modules/Topbar'; | ||
|
||
export default function AppLayout({ children }: Readonly<{ children: React.ReactNode }>) { | ||
return ( | ||
<div className="flex"> | ||
<Sidebar /> | ||
<div className="w-full"> | ||
<Topbar /> | ||
<main className="p-8">{children}</main> | ||
</div> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { Button } from '@app/components/common/Button'; | ||
import { routes } from '@app/constants'; | ||
import Link from 'next/link'; | ||
|
||
export default function People() { | ||
return ( | ||
<div> | ||
<h1 className="mb-10 text-lg font-semibold leading-6 text-navy-900">People</h1> | ||
<Button> | ||
<Link href={routes.people.addNew.personalDetails}>+ Employee</Link> | ||
</Button> | ||
</div> | ||
); | ||
} |
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import type { Metadata } from 'next'; | ||
import { Inter } from 'next/font/google'; | ||
|
||
import './globals.css'; | ||
|
||
const inter = Inter({ subsets: ['latin'] }); | ||
|
||
export const metadata: Metadata = { | ||
title: 'CPF - Career Progression Framework', | ||
description: 'Career Progression Framework Application', | ||
}; | ||
|
||
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { | ||
return ( | ||
<html lang="en"> | ||
<body className={`${inter.className} h-full bg-navy-50`}>{children}</body> | ||
</html> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1 @@ | ||
export { Button } from './Button'; | ||
export type { StyleTypes, Variants, Props } from './Button.interface'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { PropsWithChildren } from 'react'; | ||
import { FieldValues, FormProvider as RHFFormProvider, UseFormReturn } from 'react-hook-form'; | ||
|
||
interface Props<T extends FieldValues> extends PropsWithChildren { | ||
form: UseFormReturn<T>; | ||
onSubmit?: (values: T) => void; | ||
} | ||
|
||
export const FormProvider = <T extends FieldValues>({ form, onSubmit, children }: Props<T>) => { | ||
return ( | ||
<RHFFormProvider {...form}> | ||
<form {...(onSubmit && { onSubmit: form.handleSubmit(onSubmit) })}>{children}</form> | ||
</RHFFormProvider> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { FormProvider } from './FormProvider'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { FieldValues, RegisterOptions } from 'react-hook-form'; | ||
|
||
export interface InputProps { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we add some props for the icon at the beginning/end of the field? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll add input styles from Figma styleguide in the next PR. I didn't want to make this PR too big with all the extras. |
||
label?: string; | ||
placeholder?: string; | ||
name: string; | ||
options?: RegisterOptions<FieldValues, string>; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { FC, memo } from 'react'; | ||
import { InputProps } from './Input.interface'; | ||
import { useFormContext } from 'react-hook-form'; | ||
import { generateClassNames } from '@app/utils'; | ||
import { ErrorMessage } from '@hookform/error-message'; | ||
import { CheckMarkIcon } from '@app/static/icons/AlertTriangleIcon'; | ||
|
||
export const Input: FC<InputProps> = memo(({ label, name, placeholder, options = {}, ...otherProps }) => { | ||
const { formState, register, getFieldState } = useFormContext(); | ||
const error = getFieldState(name).error; | ||
|
||
return ( | ||
<div className="flex flex-col gap-y-3"> | ||
{label && ( | ||
<label className="text-navy-900" htmlFor={`input-${name}`}> | ||
{label} | ||
</label> | ||
)} | ||
<input | ||
className={generateClassNames( | ||
'outline-black h-12 w-full rounded-xl border border-navy-200 px-4 outline-none focus:border-navy-700', | ||
{ 'border-red-600 focus:border-red-600': !!error }, | ||
)} | ||
placeholder={placeholder} | ||
id={`input-${name}`} | ||
{...register(name, options)} | ||
{...otherProps} | ||
/> | ||
<ErrorMessage | ||
errors={formState.errors} | ||
name={name} | ||
render={({ message }) => ( | ||
<div className="flex items-center gap-x-2"> | ||
<CheckMarkIcon /> | ||
<div className="text-sm text-red-600 first-letter:uppercase">{message}</div> | ||
</div> | ||
)} | ||
/> | ||
</div> | ||
); | ||
}); | ||
|
||
Input.displayName = 'Input'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { Input } from './Input'; | ||
export type { InputProps } from './Input.interface'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { routes } from '@app/constants'; | ||
import { Step } from '../SideStepper'; | ||
import { AddNewPersonRouteKeys } from './EmployeeSideStepper.interface'; | ||
|
||
export const addEmployeeInitialSteps: Step<AddNewPersonRouteKeys>[] = [ | ||
{ label: '1. Personal details', state: 'notStarted', current: true, href: routes.people.addNew.personalDetails }, | ||
{ label: '2. Main ladder', state: 'notStarted', current: false, href: routes.people.addNew.mainLadder }, | ||
]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assuming it's not a one-time use, perhaps it would be worth adding this grid cold to the theme?