Skip to content

Commit

Permalink
feat: title and subtitle form (#1)
Browse files Browse the repository at this point in the history
* feat: initial form input

* feat: title and subtitle form
  • Loading branch information
StereoPT authored Jun 19, 2024
1 parent 43177c8 commit db8fa32
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 9 deletions.
91 changes: 87 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@
"lint": "next lint"
},
"dependencies": {
"@hookform/resolvers": "^3.6.0",
"clsx": "^2.1.1",
"next": "14.2.4",
"react": "^18",
"react-dom": "^18"
"react-dom": "^18",
"react-hook-form": "^7.52.0",
"tailwind-merge": "^2.3.0",
"yup": "^1.4.0"
},
"devDependencies": {
"@types/node": "^20",
Expand Down
54 changes: 54 additions & 0 deletions src/components/FormInput/FormInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useFormContext } from 'react-hook-form';

import { cn } from '@/utils/cn';

type FormInputProps = {
name: string;
label: string;
placeholder: string;
required?: boolean;
};

const FormInput = ({
name,
label,
placeholder,
required = false,
}: FormInputProps) => {
const {
register,
formState: { errors },
} = useFormContext();

const inputError = errors[name];

return (
<label htmlFor={name} className="form-control w-full max-w-xs">
<div className="label">
<span className={cn('label-text', inputError && 'text-error')}>
{label}
{required && <span className="ml-1 text-red-700">*</span>}
</span>
</div>
<input
type="text"
id={name}
{...register(name)}
placeholder={placeholder}
className={cn(
'input input-sm input-bordered w-full max-w-xs',
inputError && 'input-error',
)}
/>
{inputError && (
<div className="label">
<span className="label-text-alt text-error">
{inputError?.message as string}
</span>
</div>
)}
</label>
);
};

export default FormInput;
46 changes: 46 additions & 0 deletions src/components/MenuForm/MenuForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { FormProvider, SubmitHandler, useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';

import FormInput from '../FormInput/FormInput';
import { MenuFormInputs, menuFormSchema } from '@/schemas/menuForm';

const MenuForm = () => {
const methods = useForm<MenuFormInputs>({
resolver: yupResolver(menuFormSchema),
});

const { handleSubmit } = methods;

const onFormSubmit: SubmitHandler<MenuFormInputs> = (values) => {
alert(JSON.stringify(values, null, 2));
};

return (
<div className="basis-1/2">
<FormProvider {...methods}>
<form
className="flex flex-col items-center gap-4"
onSubmit={handleSubmit(onFormSubmit)}>
<div className="flex flex-col items-center gap-2 w-full">
<FormInput
name="title"
label="Menu Title"
placeholder="Title"
required
/>
<FormInput
name="subtitle"
label="Menu Subtitle"
placeholder="Subtitle"
/>
</div>
<div className="flex justify-end max-w-xs w-full">
<button className="btn btn-sm btn-primary">Submit</button>
</div>
</form>
</FormProvider>
</div>
);
};

export default MenuForm;
27 changes: 25 additions & 2 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,31 @@
import { ReactElement, ReactNode } from 'react';
import { NextPage } from 'next';
import Head from 'next/head';

import '@/styles/globals.css';

import type { AppProps } from 'next/app';

const App = ({ Component, pageProps }: AppProps) => {
return <Component {...pageProps} />;
type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode;
};

type AppPropsWithLayout<P = {}> = AppProps<P> & {
Component: NextPageWithLayout<P>;
};

const App = ({ Component, pageProps }: AppPropsWithLayout) => {
const getLayout = Component.getLayout ?? ((page) => page);

return (
<>
<Head>
<title>Easy-Menu</title>
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
</Head>
{getLayout(<Component {...pageProps} />)}
</>
);
};

export default App;
7 changes: 5 additions & 2 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import MenuForm from '@/components/MenuForm/MenuForm';

const Home = () => {
return (
<main>
<button className="btn btn-primary">Easy-Menu</button>
<main className="flex justify-center gap-4 p-4">
<MenuForm />
<div className="flex justify-center basis-1/2">Menu Preview</div>
</main>
);
};
Expand Down
14 changes: 14 additions & 0 deletions src/schemas/menuForm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as yup from 'yup';

const TitleMessages = {
required: 'Menu Title is required!',
};

export const menuFormSchema = yup
.object({
title: yup.string().trim().required(TitleMessages.required),
subtitle: yup.string().trim().optional(),
})
.required();

export type MenuFormInputs = yup.InferType<typeof menuFormSchema>;
6 changes: 6 additions & 0 deletions src/utils/cn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

export const cn = (...inputs: ClassValue[]) => {
return twMerge(clsx(inputs));
};

0 comments on commit db8fa32

Please sign in to comment.