From b642c7be836e18244b66e6a03dddca58dcc5f761 Mon Sep 17 00:00:00 2001 From: SeonghunYang Date: Tue, 27 Feb 2024 09:22:33 +0000 Subject: [PATCH 01/28] feat: Add form components --- app/ui/view/molecule/form/form-root.tsx | 7 +++++++ app/ui/view/molecule/form/form-text-input.tsx | 16 ++++++++++++++++ app/ui/view/molecule/form/index.tsx | 6 ++++++ 3 files changed, 29 insertions(+) create mode 100644 app/ui/view/molecule/form/form-root.tsx create mode 100644 app/ui/view/molecule/form/form-text-input.tsx create mode 100644 app/ui/view/molecule/form/index.tsx diff --git a/app/ui/view/molecule/form/form-root.tsx b/app/ui/view/molecule/form/form-root.tsx new file mode 100644 index 00000000..f8910ed0 --- /dev/null +++ b/app/ui/view/molecule/form/form-root.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +type FormRootProps = {}; + +export function FormRoot({ children }: React.PropsWithChildren) { + return
{children}
; +} diff --git a/app/ui/view/molecule/form/form-text-input.tsx b/app/ui/view/molecule/form/form-text-input.tsx new file mode 100644 index 00000000..400c3710 --- /dev/null +++ b/app/ui/view/molecule/form/form-text-input.tsx @@ -0,0 +1,16 @@ +import TextInput from '../../atom/text-input/text-input'; + +type FormTextInputProps = { + label: string; + id: string; + placeholder: string; +}; + +export function FormTextInput({ label, id, placeholder }: FormTextInputProps) { + return ( + <> + + + + ); +} diff --git a/app/ui/view/molecule/form/index.tsx b/app/ui/view/molecule/form/index.tsx new file mode 100644 index 00000000..1bfd1eb6 --- /dev/null +++ b/app/ui/view/molecule/form/index.tsx @@ -0,0 +1,6 @@ +import { FormRoot } from './form-root'; +import { FormTextInput } from './form-text-input'; + +const Form = Object.assign(FormRoot, { + TextInput: FormTextInput, +}); From 6312b6b75360d3caad26ac71dba5585949102660 Mon Sep 17 00:00:00 2001 From: SeonghunYang Date: Tue, 27 Feb 2024 09:31:57 +0000 Subject: [PATCH 02/28] refactor: Refactor form rendering and add label styling --- app/ui/view/molecule/form/form-root.tsx | 12 ++++++++- app/ui/view/molecule/form/form-text-input.tsx | 4 ++- app/ui/view/molecule/form/form.stories.tsx | 27 +++++++++++++++++++ app/ui/view/molecule/form/index.tsx | 2 ++ 4 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 app/ui/view/molecule/form/form.stories.tsx diff --git a/app/ui/view/molecule/form/form-root.tsx b/app/ui/view/molecule/form/form-root.tsx index f8910ed0..05189de0 100644 --- a/app/ui/view/molecule/form/form-root.tsx +++ b/app/ui/view/molecule/form/form-root.tsx @@ -3,5 +3,15 @@ import React from 'react'; type FormRootProps = {}; export function FormRoot({ children }: React.PropsWithChildren) { - return
{children}
; + const render = () => { + return React.Children.map(children, (child, index) => { + return ( +
+ {child} +
+ ); + }); + }; + + return
{render()}
; } diff --git a/app/ui/view/molecule/form/form-text-input.tsx b/app/ui/view/molecule/form/form-text-input.tsx index 400c3710..daa7a487 100644 --- a/app/ui/view/molecule/form/form-text-input.tsx +++ b/app/ui/view/molecule/form/form-text-input.tsx @@ -9,7 +9,9 @@ type FormTextInputProps = { export function FormTextInput({ label, id, placeholder }: FormTextInputProps) { return ( <> - + ); diff --git a/app/ui/view/molecule/form/form.stories.tsx b/app/ui/view/molecule/form/form.stories.tsx new file mode 100644 index 00000000..51eed050 --- /dev/null +++ b/app/ui/view/molecule/form/form.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import Form from '.'; + +const meta = { + title: 'ui/view/molecule/Form', + component: Form, +} as Meta; + +export default meta; +type Story = StoryObj; + +const FormTemplate: Story = { + render: () => { + return ( +
+ + + + + ); + }, +}; + +export const Default = { + ...FormTemplate, +}; diff --git a/app/ui/view/molecule/form/index.tsx b/app/ui/view/molecule/form/index.tsx index 1bfd1eb6..6953d619 100644 --- a/app/ui/view/molecule/form/index.tsx +++ b/app/ui/view/molecule/form/index.tsx @@ -4,3 +4,5 @@ import { FormTextInput } from './form-text-input'; const Form = Object.assign(FormRoot, { TextInput: FormTextInput, }); + +export default Form; From da3777078c81914fb67e5680c318fd78165a072b Mon Sep 17 00:00:00 2001 From: SeonghunYang Date: Tue, 27 Feb 2024 09:36:30 +0000 Subject: [PATCH 03/28] feat: Add number type support to TextInput component --- app/ui/view/atom/text-input/text-input.stories.tsx | 7 +++++++ app/ui/view/atom/text-input/text-input.tsx | 9 +++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/ui/view/atom/text-input/text-input.stories.tsx b/app/ui/view/atom/text-input/text-input.stories.tsx index 3fb5c78f..2b2c3868 100644 --- a/app/ui/view/atom/text-input/text-input.stories.tsx +++ b/app/ui/view/atom/text-input/text-input.stories.tsx @@ -31,6 +31,13 @@ export const Password: Story = { }, }; +export const Number: Story = { + args: { + type: 'number', + defaultValue: 123, + }, +}; + export const WithIcon: Story = { args: { defaultValue: '', diff --git a/app/ui/view/atom/text-input/text-input.tsx b/app/ui/view/atom/text-input/text-input.tsx index 8c68fbc8..7c91b123 100644 --- a/app/ui/view/atom/text-input/text-input.tsx +++ b/app/ui/view/atom/text-input/text-input.tsx @@ -4,14 +4,14 @@ import { twMerge } from 'tailwind-merge'; import { getInputColors } from '@/app/utils/style/color.util'; export interface TextInputProps extends React.InputHTMLAttributes { - type?: 'text' | 'password'; - defaultValue?: string; - value?: string; + type?: 'text' | 'password' | 'number'; + defaultValue?: string | number; + value?: string | number; icon?: React.ElementType; error?: boolean; errorMessage?: string; disabled?: boolean; - onValueChange?: (value: unknown) => void; + onValueChange?: (value: string | number) => void; } const TextInput = React.forwardRef(function TextInput( @@ -54,6 +54,7 @@ const TextInput = React.forwardRef(function Te className={twMerge( 'w-full focus:outline-none focus:ring-0 border-none bg-transparent text-sm rounded-lg transition duration-100 py-2', 'text-black-1', + '[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none', Icon ? 'pl-2' : 'pl-3', error ? 'pr-3' : 'pr-4', disabled ? 'text-gray-6 placeholder:text-gray-6' : 'placeholder:text-gray-6', From e26fd75adfcfc5965f1a6b5b0bdc1614e4c5ec18 Mon Sep 17 00:00:00 2001 From: SeonghunYang Date: Tue, 27 Feb 2024 09:45:33 +0000 Subject: [PATCH 04/28] feat: Add NumberWithPlaceholder story to text-input.stories.tsx --- app/ui/view/atom/text-input/text-input.stories.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/ui/view/atom/text-input/text-input.stories.tsx b/app/ui/view/atom/text-input/text-input.stories.tsx index 2b2c3868..97ff8291 100644 --- a/app/ui/view/atom/text-input/text-input.stories.tsx +++ b/app/ui/view/atom/text-input/text-input.stories.tsx @@ -38,6 +38,14 @@ export const Number: Story = { }, }; +export const NumberWithPlaceholder: Story = { + args: { + type: 'number', + defaultValue: '', + placeholder: 'number', + }, +}; + export const WithIcon: Story = { args: { defaultValue: '', From d3a471aca63a78b22c8afbc9771d6c5c74ebcf17 Mon Sep 17 00:00:00 2001 From: SeonghunYang Date: Tue, 27 Feb 2024 09:45:56 +0000 Subject: [PATCH 05/28] feat: Add FormSelect component --- app/ui/view/molecule/form/form-select.tsx | 23 ++++++++++++++++++++++ app/ui/view/molecule/form/form.stories.tsx | 18 ++++++++++++++++- app/ui/view/molecule/form/index.tsx | 2 ++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 app/ui/view/molecule/form/form-select.tsx diff --git a/app/ui/view/molecule/form/form-select.tsx b/app/ui/view/molecule/form/form-select.tsx new file mode 100644 index 00000000..98a4326b --- /dev/null +++ b/app/ui/view/molecule/form/form-select.tsx @@ -0,0 +1,23 @@ +import Select from '../select'; + +type FormSelectProps = { + label: string; + id: string; + options: { value: string; placeholder: string }[]; + placeholder: string; +}; + +export const FormSelect = ({ label, id, options, placeholder }: FormSelectProps) => { + return ( + <> + + + + ); +}; diff --git a/app/ui/view/molecule/form/form.stories.tsx b/app/ui/view/molecule/form/form.stories.tsx index 51eed050..0b7fa6d2 100644 --- a/app/ui/view/molecule/form/form.stories.tsx +++ b/app/ui/view/molecule/form/form.stories.tsx @@ -5,6 +5,13 @@ import Form from '.'; const meta = { title: 'ui/view/molecule/Form', component: Form, + decorators: [ + (Story) => ( +
+ +
+ ), + ], } as Meta; export default meta; @@ -16,7 +23,16 @@ const FormTemplate: Story = {
- + ); }, diff --git a/app/ui/view/molecule/form/index.tsx b/app/ui/view/molecule/form/index.tsx index 6953d619..4f75d2ff 100644 --- a/app/ui/view/molecule/form/index.tsx +++ b/app/ui/view/molecule/form/index.tsx @@ -1,8 +1,10 @@ import { FormRoot } from './form-root'; import { FormTextInput } from './form-text-input'; +import { FormSelect } from './form-select'; const Form = Object.assign(FormRoot, { TextInput: FormTextInput, + Select: FormSelect, }); export default Form; From 6f3d246862db833550bea00a537754010145e164 Mon Sep 17 00:00:00 2001 From: SeonghunYang Date: Tue, 27 Feb 2024 09:51:47 +0000 Subject: [PATCH 06/28] feat: Add form number and password inputs --- .../view/molecule/form/form-number-input.tsx | 18 ++++++++++++++++++ .../view/molecule/form/form-password-input.tsx | 18 ++++++++++++++++++ app/ui/view/molecule/form/form.stories.tsx | 3 ++- app/ui/view/molecule/form/index.tsx | 4 ++++ 4 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 app/ui/view/molecule/form/form-number-input.tsx create mode 100644 app/ui/view/molecule/form/form-password-input.tsx diff --git a/app/ui/view/molecule/form/form-number-input.tsx b/app/ui/view/molecule/form/form-number-input.tsx new file mode 100644 index 00000000..b683b1e6 --- /dev/null +++ b/app/ui/view/molecule/form/form-number-input.tsx @@ -0,0 +1,18 @@ +import TextInput from '../../atom/text-input/text-input'; + +type FormNumberInputProps = { + label: string; + id: string; + placeholder: string; +}; + +export function FormNumberInput({ label, id, placeholder }: FormNumberInputProps) { + return ( + <> + + + + ); +} diff --git a/app/ui/view/molecule/form/form-password-input.tsx b/app/ui/view/molecule/form/form-password-input.tsx new file mode 100644 index 00000000..c5f97ef3 --- /dev/null +++ b/app/ui/view/molecule/form/form-password-input.tsx @@ -0,0 +1,18 @@ +import TextInput from '../../atom/text-input/text-input'; + +type FormPasswordInputProps = { + label: string; + id: string; + placeholder: string; +}; + +export function FormPasswordInput({ label, id, placeholder }: FormPasswordInputProps) { + return ( + <> + + + + ); +} diff --git a/app/ui/view/molecule/form/form.stories.tsx b/app/ui/view/molecule/form/form.stories.tsx index 0b7fa6d2..83326805 100644 --- a/app/ui/view/molecule/form/form.stories.tsx +++ b/app/ui/view/molecule/form/form.stories.tsx @@ -22,7 +22,8 @@ const FormTemplate: Story = { return (
- + + Date: Tue, 27 Feb 2024 10:03:45 +0000 Subject: [PATCH 07/28] refactor: Refactor FormRoot component to use useFormState hook --- app/ui/view/molecule/form/form-root.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/app/ui/view/molecule/form/form-root.tsx b/app/ui/view/molecule/form/form-root.tsx index 05189de0..d847243a 100644 --- a/app/ui/view/molecule/form/form-root.tsx +++ b/app/ui/view/molecule/form/form-root.tsx @@ -1,8 +1,19 @@ import React from 'react'; +import { useFormState } from 'react-dom'; -type FormRootProps = {}; +type State = { + message: string | null; + errors: Record; +}; + +type FormRootProps = { + action: (prevState: State, formData: FormData) => State; +}; + +export function FormRoot({ action, children }: React.PropsWithChildren) { + const initialState = { message: null, errors: {} }; + const [state, dispatch] = useFormState(action, initialState); -export function FormRoot({ children }: React.PropsWithChildren) { const render = () => { return React.Children.map(children, (child, index) => { return ( @@ -13,5 +24,5 @@ export function FormRoot({ children }: React.PropsWithChildren) { }); }; - return {render()}; + return
{render()}
; } From b63971754563e851bc2fc00786f8c5096956f6e7 Mon Sep 17 00:00:00 2001 From: SeonghunYang Date: Tue, 27 Feb 2024 10:27:44 +0000 Subject: [PATCH 08/28] feat: Add FormSubmitButton component to FormRoot --- app/ui/view/molecule/form/form-root.tsx | 26 +++++++++++++++-- .../view/molecule/form/form-submit-button.tsx | 28 +++++++++++++++++++ app/ui/view/molecule/form/form.stories.tsx | 3 +- app/ui/view/molecule/form/index.tsx | 2 ++ 4 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 app/ui/view/molecule/form/form-submit-button.tsx diff --git a/app/ui/view/molecule/form/form-root.tsx b/app/ui/view/molecule/form/form-root.tsx index d847243a..fb0eaaf1 100644 --- a/app/ui/view/molecule/form/form-root.tsx +++ b/app/ui/view/molecule/form/form-root.tsx @@ -1,21 +1,36 @@ import React from 'react'; import { useFormState } from 'react-dom'; +import { FormSubmitButton } from './form-submit-button'; type State = { message: string | null; errors: Record; }; +export const filterChildrenByType = (children: React.ReactNode, elementType: React.ElementType) => { + const childArray = React.Children.toArray(children); + return childArray.filter((child) => React.isValidElement(child) && child.type === elementType); +}; + +const getFormSubmitButton = (children: React.ReactNode) => { + return filterChildrenByType(children, FormSubmitButton); +}; + type FormRootProps = { + id: string; action: (prevState: State, formData: FormData) => State; }; -export function FormRoot({ action, children }: React.PropsWithChildren) { +export function FormRoot({ id, action, children }: React.PropsWithChildren) { const initialState = { message: null, errors: {} }; const [state, dispatch] = useFormState(action, initialState); - const render = () => { + const formSubmitButton = getFormSubmitButton(children); + + const renderWithoutSubmitButton = () => { return React.Children.map(children, (child, index) => { + if (!React.isValidElement(child) || child.type === FormSubmitButton) return null; + if (child.type === FormSubmitButton) return child; return (
{child} @@ -24,5 +39,10 @@ export function FormRoot({ action, children }: React.PropsWithChildren{render()}; + return ( +
+ {renderWithoutSubmitButton()} +
{formSubmitButton}
+
+ ); } diff --git a/app/ui/view/molecule/form/form-submit-button.tsx b/app/ui/view/molecule/form/form-submit-button.tsx new file mode 100644 index 00000000..b9da549f --- /dev/null +++ b/app/ui/view/molecule/form/form-submit-button.tsx @@ -0,0 +1,28 @@ +import clsx from 'clsx'; +import Button from '../../atom/button/button'; + +type FormSubmitButtonProps = { + label: string; + position?: 'left' | 'right' | 'center'; + variant?: 'primary' | 'secondary'; + size?: 'xs' | 'sm' | 'md' | 'lg' | 'default'; +}; + +export function FormSubmitButton({ + label, + position = 'right', + variant = 'primary', + size = 'md', +}: FormSubmitButtonProps) { + return ( +
+
+ ); +} diff --git a/app/ui/view/molecule/form/form.stories.tsx b/app/ui/view/molecule/form/form.stories.tsx index 83326805..6085ce35 100644 --- a/app/ui/view/molecule/form/form.stories.tsx +++ b/app/ui/view/molecule/form/form.stories.tsx @@ -20,7 +20,7 @@ type Story = StoryObj; const FormTemplate: Story = { render: () => { return ( -
+ @@ -34,6 +34,7 @@ const FormTemplate: Story = { { value: '3', placeholder: 'three' }, ]} /> + ); }, diff --git a/app/ui/view/molecule/form/index.tsx b/app/ui/view/molecule/form/index.tsx index 7f0c9f84..54c42f76 100644 --- a/app/ui/view/molecule/form/index.tsx +++ b/app/ui/view/molecule/form/index.tsx @@ -3,12 +3,14 @@ import { FormTextInput } from './form-text-input'; import { FormSelect } from './form-select'; import { FormPasswordInput } from './form-password-input'; import { FormNumberInput } from './form-number-input'; +import { FormSubmitButton } from './form-submit-button'; const Form = Object.assign(FormRoot, { TextInput: FormTextInput, Select: FormSelect, NumberInput: FormNumberInput, PasswordInput: FormPasswordInput, + SubmitButton: FormSubmitButton, }); export default Form; From 5590017a05fd8f5f37a1363dba7b18c279992b5c Mon Sep 17 00:00:00 2001 From: SeonghunYang Date: Tue, 27 Feb 2024 10:38:20 +0000 Subject: [PATCH 09/28] feat: Add FormContext to form-root.tsx --- app/ui/view/molecule/form/form-root.tsx | 13 ++++++++----- app/ui/view/molecule/form/form-submit-button.tsx | 5 ++++- app/ui/view/molecule/form/form.context.ts | 10 ++++++++++ 3 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 app/ui/view/molecule/form/form.context.ts diff --git a/app/ui/view/molecule/form/form-root.tsx b/app/ui/view/molecule/form/form-root.tsx index fb0eaaf1..bb9f1481 100644 --- a/app/ui/view/molecule/form/form-root.tsx +++ b/app/ui/view/molecule/form/form-root.tsx @@ -1,8 +1,9 @@ import React from 'react'; import { useFormState } from 'react-dom'; import { FormSubmitButton } from './form-submit-button'; +import { FormContext } from './form.context'; -type State = { +export type State = { message: string | null; errors: Record; }; @@ -40,9 +41,11 @@ export function FormRoot({ id, action, children }: React.PropsWithChildren - {renderWithoutSubmitButton()} -
{formSubmitButton}
- + +
+ {renderWithoutSubmitButton()} +
{formSubmitButton}
+
+
); } diff --git a/app/ui/view/molecule/form/form-submit-button.tsx b/app/ui/view/molecule/form/form-submit-button.tsx index b9da549f..b0b346aa 100644 --- a/app/ui/view/molecule/form/form-submit-button.tsx +++ b/app/ui/view/molecule/form/form-submit-button.tsx @@ -1,5 +1,7 @@ import clsx from 'clsx'; import Button from '../../atom/button/button'; +import { useContext } from 'react'; +import { FormContext } from './form.context'; type FormSubmitButtonProps = { label: string; @@ -14,6 +16,7 @@ export function FormSubmitButton({ variant = 'primary', size = 'md', }: FormSubmitButtonProps) { + const { formId } = useContext(FormContext); return (
-
); } diff --git a/app/ui/view/molecule/form/form.context.ts b/app/ui/view/molecule/form/form.context.ts new file mode 100644 index 00000000..b60c6328 --- /dev/null +++ b/app/ui/view/molecule/form/form.context.ts @@ -0,0 +1,10 @@ +import { createContext } from 'react'; +import type { State } from './form-root'; + +type FormContext = State & { formId: string }; + +export const FormContext = createContext({ + formId: '', + message: null, + errors: {}, +}); From 67fe26ca709399a8f1cb86ec1aff72209cc1f46d Mon Sep 17 00:00:00 2001 From: SeonghunYang Date: Thu, 29 Feb 2024 11:25:20 +0000 Subject: [PATCH 10/28] chore: Add zod package dependency --- package-lock.json | 11 ++++++++++- package.json | 3 ++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index b4c90f4c..dcc00259 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,8 @@ "react": "^18", "react-dom": "^18", "tailwind-merge": "^2.2.1", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zod": "^3.22.4" }, "devDependencies": { "@storybook/addon-essentials": "^7.6.15", @@ -21284,6 +21285,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 5d3f2c89..3d5966cf 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "react": "^18", "react-dom": "^18", "tailwind-merge": "^2.2.1", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zod": "^3.22.4" }, "devDependencies": { "@storybook/addon-essentials": "^7.6.15", From d551ab9766c101dfd7e313f94338055262f361e6 Mon Sep 17 00:00:00 2001 From: SeonghunYang Date: Thu, 29 Feb 2024 11:40:37 +0000 Subject: [PATCH 11/28] feat: Add text input component to display multiple error messages and Icon --- .../atom/text-input/text-input.stories.tsx | 27 ++++++++++++++++++- app/ui/view/atom/text-input/text-input.tsx | 20 +++++++++++--- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/app/ui/view/atom/text-input/text-input.stories.tsx b/app/ui/view/atom/text-input/text-input.stories.tsx index 97ff8291..5c887ec3 100644 --- a/app/ui/view/atom/text-input/text-input.stories.tsx +++ b/app/ui/view/atom/text-input/text-input.stories.tsx @@ -65,7 +65,32 @@ export const WithError: Story = { args: { defaultValue: '', error: true, - errorMessage: 'error message', + errorMessages: ['error message'], + }, +}; + +export const FullTextWithError: Story = { + args: { + defaultValue: 'Full text with errorrrrrrrrrrrrrrrrrrrrrrr', + error: true, + errorMessages: ['error message'], + }, +}; + +export const WithErrors: Story = { + args: { + defaultValue: '', + error: true, + errorMessages: ['error message', 'error message'], + }, +}; + +export const WithIconAndError: Story = { + args: { + defaultValue: '', + error: true, + errorMessages: ['error message'], + icon: MagnifyingGlassIcon, }, }; diff --git a/app/ui/view/atom/text-input/text-input.tsx b/app/ui/view/atom/text-input/text-input.tsx index 7c91b123..59b9d2d0 100644 --- a/app/ui/view/atom/text-input/text-input.tsx +++ b/app/ui/view/atom/text-input/text-input.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { twMerge } from 'tailwind-merge'; import { getInputColors } from '@/app/utils/style/color.util'; +import { ExclamationCircleIcon } from '@heroicons/react/20/solid'; export interface TextInputProps extends React.InputHTMLAttributes { type?: 'text' | 'password' | 'number'; @@ -9,7 +10,7 @@ export interface TextInputProps extends React.InputHTMLAttributes void; } @@ -21,7 +22,7 @@ const TextInput = React.forwardRef(function Te value, icon, error = false, - errorMessage, + errorMessages, disabled = false, placeholder, className, @@ -56,7 +57,7 @@ const TextInput = React.forwardRef(function Te 'text-black-1', '[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none', Icon ? 'pl-2' : 'pl-3', - error ? 'pr-3' : 'pr-4', + error ? 'pr-9' : 'pr-3', disabled ? 'text-gray-6 placeholder:text-gray-6' : 'placeholder:text-gray-6', )} placeholder={placeholder} @@ -65,8 +66,19 @@ const TextInput = React.forwardRef(function Te onValueChange?.(e.target.value); }} /> + {error ? ( + + ) : null}
- {error && errorMessage ?

{errorMessage}

: null} + {error && errorMessages + ? errorMessages.map((message, index) => ( +

+ {message} +

+ )) + : null} ); }); From a63472e54816cf2cbd280fb8c880483e314eeade Mon Sep 17 00:00:00 2001 From: SeonghunYang Date: Thu, 29 Feb 2024 11:47:05 +0000 Subject: [PATCH 12/28] refactor: Refactor Select component to support multiple error messages --- app/ui/view/molecule/select/select-root.tsx | 12 +++++++++--- app/ui/view/molecule/select/select.stories.tsx | 13 +++++++++++-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/app/ui/view/molecule/select/select-root.tsx b/app/ui/view/molecule/select/select-root.tsx index 2afc97d7..b39d8969 100644 --- a/app/ui/view/molecule/select/select-root.tsx +++ b/app/ui/view/molecule/select/select-root.tsx @@ -10,14 +10,14 @@ export interface SelectProps extends React.HTMLAttributes { name?: string; icon?: React.ElementType; error?: boolean; - errorMessage?: string; + errorMessages?: string[]; disabled?: boolean; children?: React.ReactNode; onValueChange?: (value: unknown) => void; } export const SelectRoot = React.forwardRef(function Select( - { defaultValue, icon, error = false, errorMessage, disabled = false, name, children, placeholder, onValueChange }, + { defaultValue, icon, error = false, errorMessages, disabled = false, name, children, placeholder, onValueChange }, ref, ) { const [selectedValue, setSelectedValue] = useState(defaultValue); @@ -85,7 +85,13 @@ export const SelectRoot = React.forwardRef(functi - {error && errorMessage ?

{errorMessage}

: null} + {error && errorMessages + ? errorMessages.map((message, index) => ( +

+ {message} +

+ )) + : null} ); }); diff --git a/app/ui/view/molecule/select/select.stories.tsx b/app/ui/view/molecule/select/select.stories.tsx index 7caff20e..666b64dd 100644 --- a/app/ui/view/molecule/select/select.stories.tsx +++ b/app/ui/view/molecule/select/select.stories.tsx @@ -23,7 +23,7 @@ type SelectTemplateProps = { defaultValue?: string; icon?: React.ElementType; error?: boolean; - errorMessage?: string; + errorMessages?: string[]; disabled?: boolean; }; @@ -75,6 +75,15 @@ export const WithError = { args: { placeholder: 'Select..', error: true, - errorMessage: 'error message', + errorMessages: ['error message'], + }, +}; + +export const WithErrors = { + ...SelectTemplate, + args: { + placeholder: 'Select..', + error: true, + errorMessages: ['error message', 'error message'], }, }; From 2e07a21a5c4ddb7e3293fdba6445459edbb41a36 Mon Sep 17 00:00:00 2001 From: SeonghunYang Date: Thu, 29 Feb 2024 11:54:13 +0000 Subject: [PATCH 13/28] feat: Add form error handler --- app/business/auth/user.command.ts | 46 +++++++++++++++++++ .../view/molecule/form/form-number-input.tsx | 13 +++++- .../molecule/form/form-password-input.tsx | 13 +++++- app/ui/view/molecule/form/form-root.tsx | 4 +- app/ui/view/molecule/form/form-select.tsx | 6 ++- app/ui/view/molecule/form/form-text-input.tsx | 13 +++++- app/ui/view/molecule/form/form.stories.tsx | 11 +++-- 7 files changed, 95 insertions(+), 11 deletions(-) create mode 100644 app/business/auth/user.command.ts diff --git a/app/business/auth/user.command.ts b/app/business/auth/user.command.ts new file mode 100644 index 00000000..6982f46c --- /dev/null +++ b/app/business/auth/user.command.ts @@ -0,0 +1,46 @@ +'use server'; + +import { State } from '@/app/ui/view/molecule/form/form-root'; +import { z } from 'zod'; + +const SimpleSignUpFormSchema = z.object({ + studentNumber: z.number({ + invalid_type_error: 'Please enter your student number.', + }), + name: z.string({ + invalid_type_error: 'Please enter your user ID.', + }), + // password: z.string(), + // amount: z.coerce + // .number() + // .gt(0, { message: 'Please enter an amount greater than $0.' }), + // status: z.enum(['pending', 'paid'], { + // invalid_type_error: 'Please select an invoice status.', + // }), + // date: z.string(), +}); + +type User = z.infer; + +export async function createUser(prevState: State, formData: FormData): Promise { + // Validate form fields using Zod + console.log('formData', formData.get('studentNumber')); + console.log('formData', formData.get('name')); + const validatedFields = SimpleSignUpFormSchema.safeParse({ + studentNumber: formData.get('studentNumber'), + name: formData.get('name'), + }); + + console.log(validatedFields); + if (!validatedFields.success) { + return { + errors: validatedFields.error.flatten().fieldErrors, + message: 'error', + }; + } + + return { + errors: {}, + message: 'blacnk', + }; +} diff --git a/app/ui/view/molecule/form/form-number-input.tsx b/app/ui/view/molecule/form/form-number-input.tsx index b683b1e6..d984c28c 100644 --- a/app/ui/view/molecule/form/form-number-input.tsx +++ b/app/ui/view/molecule/form/form-number-input.tsx @@ -1,4 +1,6 @@ import TextInput from '../../atom/text-input/text-input'; +import { FormContext } from './form.context'; +import { useContext } from 'react'; type FormNumberInputProps = { label: string; @@ -7,12 +9,21 @@ type FormNumberInputProps = { }; export function FormNumberInput({ label, id, placeholder }: FormNumberInputProps) { + const { errors } = useContext(FormContext); + return ( <> - + ); } diff --git a/app/ui/view/molecule/form/form-password-input.tsx b/app/ui/view/molecule/form/form-password-input.tsx index c5f97ef3..881da9d2 100644 --- a/app/ui/view/molecule/form/form-password-input.tsx +++ b/app/ui/view/molecule/form/form-password-input.tsx @@ -1,4 +1,6 @@ import TextInput from '../../atom/text-input/text-input'; +import { FormContext } from './form.context'; +import { useContext } from 'react'; type FormPasswordInputProps = { label: string; @@ -7,12 +9,21 @@ type FormPasswordInputProps = { }; export function FormPasswordInput({ label, id, placeholder }: FormPasswordInputProps) { + const { errors } = useContext(FormContext); + return ( <> - + ); } diff --git a/app/ui/view/molecule/form/form-root.tsx b/app/ui/view/molecule/form/form-root.tsx index bb9f1481..82012fb1 100644 --- a/app/ui/view/molecule/form/form-root.tsx +++ b/app/ui/view/molecule/form/form-root.tsx @@ -5,7 +5,7 @@ import { FormContext } from './form.context'; export type State = { message: string | null; - errors: Record; + errors: Record; }; export const filterChildrenByType = (children: React.ReactNode, elementType: React.ElementType) => { @@ -19,7 +19,7 @@ const getFormSubmitButton = (children: React.ReactNode) => { type FormRootProps = { id: string; - action: (prevState: State, formData: FormData) => State; + action: (prevState: State, formData: FormData) => Promise | State; }; export function FormRoot({ id, action, children }: React.PropsWithChildren) { diff --git a/app/ui/view/molecule/form/form-select.tsx b/app/ui/view/molecule/form/form-select.tsx index 98a4326b..728d1f42 100644 --- a/app/ui/view/molecule/form/form-select.tsx +++ b/app/ui/view/molecule/form/form-select.tsx @@ -1,4 +1,6 @@ import Select from '../select'; +import { FormContext } from './form.context'; +import { useContext } from 'react'; type FormSelectProps = { label: string; @@ -8,12 +10,14 @@ type FormSelectProps = { }; export const FormSelect = ({ label, id, options, placeholder }: FormSelectProps) => { + const { errors } = useContext(FormContext); + return ( <> - {options.map((option) => ( ))} diff --git a/app/ui/view/molecule/form/form-text-input.tsx b/app/ui/view/molecule/form/form-text-input.tsx index daa7a487..8db1a7fb 100644 --- a/app/ui/view/molecule/form/form-text-input.tsx +++ b/app/ui/view/molecule/form/form-text-input.tsx @@ -1,4 +1,6 @@ import TextInput from '../../atom/text-input/text-input'; +import { FormContext } from './form.context'; +import { useContext } from 'react'; type FormTextInputProps = { label: string; @@ -7,12 +9,21 @@ type FormTextInputProps = { }; export function FormTextInput({ label, id, placeholder }: FormTextInputProps) { + const { errors } = useContext(FormContext); + return ( <> - + ); } diff --git a/app/ui/view/molecule/form/form.stories.tsx b/app/ui/view/molecule/form/form.stories.tsx index 6085ce35..7efd5cfa 100644 --- a/app/ui/view/molecule/form/form.stories.tsx +++ b/app/ui/view/molecule/form/form.stories.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import Form from '.'; +import { createUser } from '@/app/business/auth/user.command'; const meta = { title: 'ui/view/molecule/Form', @@ -20,11 +21,11 @@ type Story = StoryObj; const FormTemplate: Story = { render: () => { return ( -
+ + - - - */} + {/* + /> */} ); From 95ea9830025ef6cbcd26f792d59b7f8af8a67a07 Mon Sep 17 00:00:00 2001 From: SeonghunYang Date: Thu, 29 Feb 2024 12:30:30 +0000 Subject: [PATCH 14/28] feat: Refactor form validation and add password confirmation check --- app/business/auth/user.command.ts | 37 +++++++++++----------- app/ui/view/atom/text-input/text-input.tsx | 2 +- app/ui/view/molecule/form/form.stories.tsx | 3 +- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/app/business/auth/user.command.ts b/app/business/auth/user.command.ts index 6982f46c..90f6d2f2 100644 --- a/app/business/auth/user.command.ts +++ b/app/business/auth/user.command.ts @@ -3,32 +3,33 @@ import { State } from '@/app/ui/view/molecule/form/form-root'; import { z } from 'zod'; -const SimpleSignUpFormSchema = z.object({ - studentNumber: z.number({ - invalid_type_error: 'Please enter your student number.', - }), - name: z.string({ - invalid_type_error: 'Please enter your user ID.', - }), - // password: z.string(), - // amount: z.coerce - // .number() - // .gt(0, { message: 'Please enter an amount greater than $0.' }), - // status: z.enum(['pending', 'paid'], { - // invalid_type_error: 'Please select an invoice status.', - // }), - // date: z.string(), -}); +const SimpleSignUpFormSchema = z + .object({ + studentNumber: z.string(), + name: z.string(), + password: z.string(), + confirmPassword: z.string(), + }) + .superRefine(({ confirmPassword, password }, ctx) => { + console.log('refind', confirmPassword, password); + if (confirmPassword !== password) { + ctx.addIssue({ + code: 'custom', + message: 'The passwords did not match', + path: ['confirmPassword'], + }); + } + }); type User = z.infer; export async function createUser(prevState: State, formData: FormData): Promise { // Validate form fields using Zod - console.log('formData', formData.get('studentNumber')); - console.log('formData', formData.get('name')); const validatedFields = SimpleSignUpFormSchema.safeParse({ studentNumber: formData.get('studentNumber'), name: formData.get('name'), + password: formData.get('password'), + confirmPassword: formData.get('confirmPassword'), }); console.log(validatedFields); diff --git a/app/ui/view/atom/text-input/text-input.tsx b/app/ui/view/atom/text-input/text-input.tsx index 59b9d2d0..8b2efe0e 100644 --- a/app/ui/view/atom/text-input/text-input.tsx +++ b/app/ui/view/atom/text-input/text-input.tsx @@ -12,7 +12,7 @@ export interface TextInputProps extends React.InputHTMLAttributes void; + onValueChange?: (value: string) => void; } const TextInput = React.forwardRef(function TextInput( diff --git a/app/ui/view/molecule/form/form.stories.tsx b/app/ui/view/molecule/form/form.stories.tsx index 7efd5cfa..ee2b10d8 100644 --- a/app/ui/view/molecule/form/form.stories.tsx +++ b/app/ui/view/molecule/form/form.stories.tsx @@ -24,7 +24,8 @@ const FormTemplate: Story = {
- {/* */} + + {/* Date: Thu, 29 Feb 2024 12:57:07 +0000 Subject: [PATCH 15/28] feat: Add validation sign-up-form --- app/business/auth/user.command.ts | 18 +++++++++++++++--- app/ui/view/molecule/form/form.stories.tsx | 19 ++++++++++--------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/app/business/auth/user.command.ts b/app/business/auth/user.command.ts index 90f6d2f2..97bdadf3 100644 --- a/app/business/auth/user.command.ts +++ b/app/business/auth/user.command.ts @@ -5,10 +5,22 @@ import { z } from 'zod'; const SimpleSignUpFormSchema = z .object({ - studentNumber: z.string(), - name: z.string(), - password: z.string(), + userId: z + .string() + .min(6, { + message: 'User ID must be at least 6 characters', + }) + .max(20, { + message: 'User ID must be at most 20 characters', + }), + password: z.string().regex(/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!^%*#?&])[A-Za-z\d@$!^%*#?&]{8,}$/, { + message: 'Password must contain at least 8 characters, one letter, one number and one special character', + }), confirmPassword: z.string(), + studentNumber: z.string().length(8, { message: '학번은 8자리 입니다' }).startsWith('60', { + message: '학번은 60으로 시작합니다', + }), + english: z.enum(['basic', 'level12', 'level34', 'bypass']), }) .superRefine(({ confirmPassword, password }, ctx) => { console.log('refind', confirmPassword, password); diff --git a/app/ui/view/molecule/form/form.stories.tsx b/app/ui/view/molecule/form/form.stories.tsx index ee2b10d8..95102794 100644 --- a/app/ui/view/molecule/form/form.stories.tsx +++ b/app/ui/view/molecule/form/form.stories.tsx @@ -22,20 +22,21 @@ const FormTemplate: Story = { render: () => { return ( - - - - - {/* + + + + */} + /> ); From fab36918cf76f536aac7aaf1a13d5b817b1d210b Mon Sep 17 00:00:00 2001 From: SeonghunYang Date: Thu, 29 Feb 2024 13:08:47 +0000 Subject: [PATCH 16/28] [web] chore: Install storybook testing library --- package-lock.json | 360 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 4 +- 2 files changed, 358 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index dcc00259..b165a290 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,13 +25,15 @@ }, "devDependencies": { "@storybook/addon-essentials": "^7.6.15", - "@storybook/addon-interactions": "^7.6.15", + "@storybook/addon-interactions": "^7.6.17", "@storybook/addon-links": "^7.6.15", "@storybook/addon-onboarding": "^1.0.11", "@storybook/blocks": "^7.6.15", + "@storybook/jest": "^0.2.3", "@storybook/nextjs": "^7.6.15", "@storybook/react": "^7.6.15", "@storybook/test": "^7.6.15", + "@storybook/testing-library": "^0.2.2", "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.2.1", "@testing-library/user-event": "^14.5.2", @@ -4714,13 +4716,13 @@ } }, "node_modules/@storybook/addon-interactions": { - "version": "7.6.16", - "resolved": "https://registry.npmjs.org/@storybook/addon-interactions/-/addon-interactions-7.6.16.tgz", - "integrity": "sha512-nOjbQCqX+u3yAwlaGHQZCoSkdsvctfiWxcUjMwDBdky2vPKUGLpfJs65w31ay/UgeR+ZWYROGwVMkOHzB4GOIA==", + "version": "7.6.17", + "resolved": "https://registry.npmjs.org/@storybook/addon-interactions/-/addon-interactions-7.6.17.tgz", + "integrity": "sha512-6zlX+RDQ1PlA6fp7C+hun8t7h2RXfCGs5dGrhEenp2lqnR/rYuUJRC0tmKpkZBb8kZVcbSChzkB/JYkBjBCzpQ==", "dev": true, "dependencies": { "@storybook/global": "^5.0.0", - "@storybook/types": "7.6.16", + "@storybook/types": "7.6.17", "jest-mock": "^27.0.6", "polished": "^4.2.2", "ts-dedent": "^2.2.0" @@ -4746,6 +4748,66 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/@storybook/addon-interactions/node_modules/@storybook/channels": { + "version": "7.6.17", + "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.17.tgz", + "integrity": "sha512-GFG40pzaSxk1hUr/J/TMqW5AFDDPUSu+HkeE/oqSWJbOodBOLJzHN6CReJS6y1DjYSZLNFt1jftPWZZInG/XUA==", + "dev": true, + "dependencies": { + "@storybook/client-logger": "7.6.17", + "@storybook/core-events": "7.6.17", + "@storybook/global": "^5.0.0", + "qs": "^6.10.0", + "telejson": "^7.2.0", + "tiny-invariant": "^1.3.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/addon-interactions/node_modules/@storybook/client-logger": { + "version": "7.6.17", + "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.17.tgz", + "integrity": "sha512-6WBYqixAXNAXlSaBWwgljWpAu10tPRBJrcFvx2gPUne58EeMM20Gi/iHYBz2kMCY+JLAgeIH7ZxInqwO8vDwiQ==", + "dev": true, + "dependencies": { + "@storybook/global": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/addon-interactions/node_modules/@storybook/core-events": { + "version": "7.6.17", + "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.17.tgz", + "integrity": "sha512-AriWMCm/k1cxlv10f+jZ1wavThTRpLaN3kY019kHWbYT9XgaSuLU67G7GPr3cGnJ6HuA6uhbzu8qtqVCd6OfXA==", + "dev": true, + "dependencies": { + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/addon-interactions/node_modules/@storybook/types": { + "version": "7.6.17", + "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.17.tgz", + "integrity": "sha512-GRY0xEJQ0PrL7DY2qCNUdIfUOE0Gsue6N+GBJw9ku1IUDFLJRDOF+4Dx2BvYcVCPI5XPqdWKlEyZdMdKjiQN7Q==", + "dev": true, + "dependencies": { + "@storybook/channels": "7.6.17", + "@types/babel__core": "^7.0.0", + "@types/express": "^4.7.0", + "file-system-cache": "2.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, "node_modules/@storybook/addon-interactions/node_modules/@types/yargs": { "version": "16.0.9", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", @@ -5426,6 +5488,124 @@ "url": "https://opencollective.com/storybook" } }, + "node_modules/@storybook/expect": { + "version": "28.1.3-5", + "resolved": "https://registry.npmjs.org/@storybook/expect/-/expect-28.1.3-5.tgz", + "integrity": "sha512-lS1oJnY1qTAxnH87C765NdfvGhksA6hBcbUVI5CHiSbNsEtr456wtg/z+dT9XlPriq1D5t2SgfNL9dBAoIGyIA==", + "dev": true, + "dependencies": { + "@types/jest": "28.1.3" + } + }, + "node_modules/@storybook/expect/node_modules/@jest/schemas": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz", + "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.24.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@storybook/expect/node_modules/@sinclair/typebox": { + "version": "0.24.51", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", + "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==", + "dev": true + }, + "node_modules/@storybook/expect/node_modules/@types/jest": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-28.1.3.tgz", + "integrity": "sha512-Tsbjk8Y2hkBaY/gJsataeb4q9Mubw9EOz7+4RjPkzD5KjTvHHs7cpws22InaoXxAVAhF5HfFbzJjo6oKWqSZLw==", + "dev": true, + "dependencies": { + "jest-matcher-utils": "^28.0.0", + "pretty-format": "^28.0.0" + } + }, + "node_modules/@storybook/expect/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@storybook/expect/node_modules/diff-sequences": { + "version": "28.1.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz", + "integrity": "sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==", + "dev": true, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@storybook/expect/node_modules/jest-diff": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz", + "integrity": "sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^28.1.1", + "jest-get-type": "^28.0.2", + "pretty-format": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@storybook/expect/node_modules/jest-get-type": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz", + "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==", + "dev": true, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@storybook/expect/node_modules/jest-matcher-utils": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.3.tgz", + "integrity": "sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^28.1.3", + "jest-get-type": "^28.0.2", + "pretty-format": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@storybook/expect/node_modules/pretty-format": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz", + "integrity": "sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==", + "dev": true, + "dependencies": { + "@jest/schemas": "^28.1.3", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@storybook/expect/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "node_modules/@storybook/global": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@storybook/global/-/global-5.0.0.tgz", @@ -5451,6 +5631,165 @@ "url": "https://opencollective.com/storybook" } }, + "node_modules/@storybook/jest": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@storybook/jest/-/jest-0.2.3.tgz", + "integrity": "sha512-ov5izrmbAFObzKeh9AOC5MlmFxAcf0o5i6YFGae9sDx6DGh6alXsRM+chIbucVkUwVHVlSzdfbLDEFGY/ShaYw==", + "dev": true, + "dependencies": { + "@storybook/expect": "storybook-jest", + "@testing-library/jest-dom": "^6.1.2", + "@types/jest": "28.1.3", + "jest-mock": "^27.3.0" + } + }, + "node_modules/@storybook/jest/node_modules/@jest/schemas": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz", + "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.24.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@storybook/jest/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@storybook/jest/node_modules/@sinclair/typebox": { + "version": "0.24.51", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", + "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==", + "dev": true + }, + "node_modules/@storybook/jest/node_modules/@types/jest": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-28.1.3.tgz", + "integrity": "sha512-Tsbjk8Y2hkBaY/gJsataeb4q9Mubw9EOz7+4RjPkzD5KjTvHHs7cpws22InaoXxAVAhF5HfFbzJjo6oKWqSZLw==", + "dev": true, + "dependencies": { + "jest-matcher-utils": "^28.0.0", + "pretty-format": "^28.0.0" + } + }, + "node_modules/@storybook/jest/node_modules/@types/yargs": { + "version": "16.0.9", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", + "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@storybook/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@storybook/jest/node_modules/diff-sequences": { + "version": "28.1.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz", + "integrity": "sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==", + "dev": true, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@storybook/jest/node_modules/jest-diff": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz", + "integrity": "sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^28.1.1", + "jest-get-type": "^28.0.2", + "pretty-format": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@storybook/jest/node_modules/jest-get-type": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz", + "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==", + "dev": true, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@storybook/jest/node_modules/jest-matcher-utils": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.3.tgz", + "integrity": "sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^28.1.3", + "jest-get-type": "^28.0.2", + "pretty-format": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@storybook/jest/node_modules/jest-mock": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", + "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", + "dev": true, + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@storybook/jest/node_modules/pretty-format": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz", + "integrity": "sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==", + "dev": true, + "dependencies": { + "@jest/schemas": "^28.1.3", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@storybook/jest/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "node_modules/@storybook/manager": { "version": "7.6.16", "resolved": "https://registry.npmjs.org/@storybook/manager/-/manager-7.6.16.tgz", @@ -5905,6 +6244,17 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@storybook/testing-library": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@storybook/testing-library/-/testing-library-0.2.2.tgz", + "integrity": "sha512-L8sXFJUHmrlyU2BsWWZGuAjv39Jl1uAqUHdxmN42JY15M4+XCMjGlArdCCjDe1wpTSW6USYISA9axjZojgtvnw==", + "dev": true, + "dependencies": { + "@testing-library/dom": "^9.0.0", + "@testing-library/user-event": "^14.4.0", + "ts-dedent": "^2.2.0" + } + }, "node_modules/@storybook/theming": { "version": "7.6.16", "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.6.16.tgz", diff --git a/package.json b/package.json index 3d5966cf..906aa630 100644 --- a/package.json +++ b/package.json @@ -36,13 +36,15 @@ }, "devDependencies": { "@storybook/addon-essentials": "^7.6.15", - "@storybook/addon-interactions": "^7.6.15", + "@storybook/addon-interactions": "^7.6.17", "@storybook/addon-links": "^7.6.15", "@storybook/addon-onboarding": "^1.0.11", "@storybook/blocks": "^7.6.15", + "@storybook/jest": "^0.2.3", "@storybook/nextjs": "^7.6.15", "@storybook/react": "^7.6.15", "@storybook/test": "^7.6.15", + "@storybook/testing-library": "^0.2.2", "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.2.1", "@testing-library/user-event": "^14.5.2", From 6fd6210090322d8b71eb6b3976d331238f34b7af Mon Sep 17 00:00:00 2001 From: SeonghunYang Date: Thu, 29 Feb 2024 13:32:33 +0000 Subject: [PATCH 17/28] refactor: Refactor SelectRoot component to include a hidden select element --- app/ui/view/molecule/select/select-root.tsx | 51 +++++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/app/ui/view/molecule/select/select-root.tsx b/app/ui/view/molecule/select/select-root.tsx index b39d8969..62c35801 100644 --- a/app/ui/view/molecule/select/select-root.tsx +++ b/app/ui/view/molecule/select/select-root.tsx @@ -1,6 +1,6 @@ import { Listbox, Transition } from '@headlessui/react'; import { ChevronUpDownIcon } from '@heroicons/react/16/solid'; -import React, { useMemo, useState } from 'react'; +import React, { useMemo, useRef, useState } from 'react'; import { twMerge } from 'tailwind-merge'; import { getInputColors } from '@/app/utils/style/color.util'; @@ -17,12 +17,24 @@ export interface SelectProps extends React.HTMLAttributes { } export const SelectRoot = React.forwardRef(function Select( - { defaultValue, icon, error = false, errorMessages, disabled = false, name, children, placeholder, onValueChange }, + { + defaultValue, + icon, + error = false, + errorMessages, + disabled = false, + name, + children, + placeholder, + id, + onValueChange, + }, ref, ) { const [selectedValue, setSelectedValue] = useState(defaultValue); + const listboxButtonRef = useRef(null); + const childrenArray = React.Children.toArray(children); const Icon = icon; - console.log(selectedValue); const selectedPlaceholder = useMemo(() => { const reactElementChildren = React.Children.toArray(children).filter( @@ -35,10 +47,40 @@ export const SelectRoot = React.forwardRef(functi return (
+ { onValueChange?.(value); setSelectedValue(value); @@ -48,6 +90,7 @@ export const SelectRoot = React.forwardRef(functi className="relative" > Date: Thu, 29 Feb 2024 13:37:25 +0000 Subject: [PATCH 18/28] feat: Add full play interaction stories --- app/business/auth/user.command.ts | 14 ++++-- .../view/molecule/form/form-submit-button.tsx | 2 +- app/ui/view/molecule/form/form.stories.tsx | 49 +++++++++++++++++-- 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/app/business/auth/user.command.ts b/app/business/auth/user.command.ts index 97bdadf3..ef9bd585 100644 --- a/app/business/auth/user.command.ts +++ b/app/business/auth/user.command.ts @@ -36,12 +36,12 @@ const SimpleSignUpFormSchema = z type User = z.infer; export async function createUser(prevState: State, formData: FormData): Promise { - // Validate form fields using Zod const validatedFields = SimpleSignUpFormSchema.safeParse({ - studentNumber: formData.get('studentNumber'), - name: formData.get('name'), + userId: formData.get('userId'), password: formData.get('password'), confirmPassword: formData.get('confirmPassword'), + studentNumber: formData.get('studentNumber'), + english: formData.get('english'), }); console.log(validatedFields); @@ -52,6 +52,14 @@ export async function createUser(prevState: State, formData: FormData): Promise< }; } + // Call the API to create a user + // but now mock the response + await new Promise((resolve) => { + setTimeout(() => { + resolve(''); + }, 2000); + }); + return { errors: {}, message: 'blacnk', diff --git a/app/ui/view/molecule/form/form-submit-button.tsx b/app/ui/view/molecule/form/form-submit-button.tsx index b0b346aa..43cad6f8 100644 --- a/app/ui/view/molecule/form/form-submit-button.tsx +++ b/app/ui/view/molecule/form/form-submit-button.tsx @@ -25,7 +25,7 @@ export function FormSubmitButton({ 'justify-end': position === 'right', })} > -
); } diff --git a/app/ui/view/molecule/form/form.stories.tsx b/app/ui/view/molecule/form/form.stories.tsx index 95102794..737138d2 100644 --- a/app/ui/view/molecule/form/form.stories.tsx +++ b/app/ui/view/molecule/form/form.stories.tsx @@ -2,6 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import Form from '.'; import { createUser } from '@/app/business/auth/user.command'; +import { userEvent, within } from '@storybook/testing-library'; const meta = { title: 'ui/view/molecule/Form', @@ -18,7 +19,7 @@ const meta = { export default meta; type Story = StoryObj; -const FormTemplate: Story = { +export const SignUpForm: Story = { render: () => { return (
@@ -41,8 +42,48 @@ const FormTemplate: Story = {
); }, -}; -export const Default = { - ...FormTemplate, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const userIdInput = canvas.getByLabelText('아이디', { + selector: 'input', + }); + + await userEvent.type(userIdInput, 'mju-graduate', { + delay: 100, + }); + + const passwordInput = canvas.getByLabelText('비밀번호', { + selector: 'input', + }); + + await userEvent.type(passwordInput, 'qw!102761', { + delay: 100, + }); + + const confirmPasswordInput = canvas.getByLabelText('비밀번호 확인', { + selector: 'input', + }); + + await userEvent.type(confirmPasswordInput, 'qw!102761', { + delay: 100, + }); + + const studentNumberInput = canvas.getByLabelText('학번', { + selector: 'input', + }); + + await userEvent.type(studentNumberInput, '60123456', { + delay: 100, + }); + + await userEvent.selectOptions(canvas.getByLabelText('영어', { selector: 'select' }), 'level12', { + delay: 100, + }); + + const submitButton = canvas.getByRole('button', { name: 'submit-button' }); + + await userEvent.click(submitButton); + }, }; From 96840f1cddb6f3dc8fd0b9d551e1a3518eddb3e8 Mon Sep 17 00:00:00 2001 From: SeonghunYang Date: Thu, 29 Feb 2024 14:48:07 +0000 Subject: [PATCH 19/28] feat: Update form inputs to disable during pending state --- app/business/auth/user.command.ts | 2 +- app/ui/view/molecule/form/form-number-input.tsx | 3 +++ app/ui/view/molecule/form/form-password-input.tsx | 3 +++ app/ui/view/molecule/form/form-select.tsx | 11 ++++++++++- app/ui/view/molecule/form/form-submit-button.tsx | 13 ++++++++++++- app/ui/view/molecule/form/form-text-input.tsx | 3 +++ app/ui/view/molecule/form/form.stories.tsx | 9 ++++++++- 7 files changed, 40 insertions(+), 4 deletions(-) diff --git a/app/business/auth/user.command.ts b/app/business/auth/user.command.ts index ef9bd585..53eb379b 100644 --- a/app/business/auth/user.command.ts +++ b/app/business/auth/user.command.ts @@ -57,7 +57,7 @@ export async function createUser(prevState: State, formData: FormData): Promise< await new Promise((resolve) => { setTimeout(() => { resolve(''); - }, 2000); + }, 3000); }); return { diff --git a/app/ui/view/molecule/form/form-number-input.tsx b/app/ui/view/molecule/form/form-number-input.tsx index d984c28c..77b969ed 100644 --- a/app/ui/view/molecule/form/form-number-input.tsx +++ b/app/ui/view/molecule/form/form-number-input.tsx @@ -1,6 +1,7 @@ import TextInput from '../../atom/text-input/text-input'; import { FormContext } from './form.context'; import { useContext } from 'react'; +import { useFormStatus } from 'react-dom'; type FormNumberInputProps = { label: string; @@ -10,6 +11,7 @@ type FormNumberInputProps = { export function FormNumberInput({ label, id, placeholder }: FormNumberInputProps) { const { errors } = useContext(FormContext); + const { pending } = useFormStatus(); return ( <> @@ -17,6 +19,7 @@ export function FormNumberInput({ label, id, placeholder }: FormNumberInputProps {label} @@ -17,6 +19,7 @@ export function FormPasswordInput({ label, id, placeholder }: FormPasswordInputP {label} { const { errors } = useContext(FormContext); + const { pending } = useFormStatus(); return ( <> - {options.map((option) => ( ))} diff --git a/app/ui/view/molecule/form/form-submit-button.tsx b/app/ui/view/molecule/form/form-submit-button.tsx index 43cad6f8..94c23431 100644 --- a/app/ui/view/molecule/form/form-submit-button.tsx +++ b/app/ui/view/molecule/form/form-submit-button.tsx @@ -2,6 +2,7 @@ import clsx from 'clsx'; import Button from '../../atom/button/button'; import { useContext } from 'react'; import { FormContext } from './form.context'; +import { useFormStatus } from 'react-dom'; type FormSubmitButtonProps = { label: string; @@ -17,6 +18,8 @@ export function FormSubmitButton({ size = 'md', }: FormSubmitButtonProps) { const { formId } = useContext(FormContext); + const { pending } = useFormStatus(); + return (
-
); } diff --git a/app/ui/view/molecule/form/form-text-input.tsx b/app/ui/view/molecule/form/form-text-input.tsx index 8db1a7fb..c77dcfd5 100644 --- a/app/ui/view/molecule/form/form-text-input.tsx +++ b/app/ui/view/molecule/form/form-text-input.tsx @@ -1,6 +1,7 @@ import TextInput from '../../atom/text-input/text-input'; import { FormContext } from './form.context'; import { useContext } from 'react'; +import { useFormStatus } from 'react-dom'; type FormTextInputProps = { label: string; @@ -10,6 +11,7 @@ type FormTextInputProps = { export function FormTextInput({ label, id, placeholder }: FormTextInputProps) { const { errors } = useContext(FormContext); + const { pending } = useFormStatus(); return ( <> @@ -17,6 +19,7 @@ export function FormTextInput({ label, id, placeholder }: FormTextInputProps) { {label} ; -export const SignUpForm: Story = { +const SingUpFormTemplate: Story = { render: () => { return (
@@ -42,7 +42,14 @@ export const SignUpForm: Story = {
); }, +}; + +export const SignUpForm: Story = { + ...SingUpFormTemplate, +}; +export const SignUpFormWithInteraction: Story = { + ...SingUpFormTemplate, play: async ({ canvasElement }) => { const canvas = within(canvasElement); From 605839aad4d2fb99925770131565efc7baed7122 Mon Sep 17 00:00:00 2001 From: SeonghunYang Date: Thu, 29 Feb 2024 15:13:26 +0000 Subject: [PATCH 20/28] feat: Add disabled and loading state to Button --- app/ui/view/atom/button/button.stories.tsx | 31 ++++++++++++++++------ app/ui/view/atom/button/button.tsx | 29 ++++++++++++++++++-- app/ui/view/atom/loading-spinner.tsx | 10 +++++++ 3 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 app/ui/view/atom/loading-spinner.tsx diff --git a/app/ui/view/atom/button/button.stories.tsx b/app/ui/view/atom/button/button.stories.tsx index 92a0c297..3e0ed3d7 100644 --- a/app/ui/view/atom/button/button.stories.tsx +++ b/app/ui/view/atom/button/button.stories.tsx @@ -52,39 +52,54 @@ const meta = { } satisfies Meta; export default meta; +type Story = StoryObj; -export const PrimaryButton: StoryObj = { +export const PrimaryButton: Story = { args: { size: 'md', variant: 'primary', label: '수강현황 자세히보기', }, - render: (args) => ); diff --git a/app/ui/view/atom/loading-spinner.tsx b/app/ui/view/atom/loading-spinner.tsx new file mode 100644 index 00000000..65f70e2b --- /dev/null +++ b/app/ui/view/atom/loading-spinner.tsx @@ -0,0 +1,10 @@ +import React from 'react'; + +const LoadingSpinner = ({ ...props }) => ( + + + + +); + +export default LoadingSpinner; From 3549a70471b7f593d59e7df84e3fe855101dd810 Mon Sep 17 00:00:00 2001 From: SeonghunYang Date: Thu, 29 Feb 2024 15:16:48 +0000 Subject: [PATCH 21/28] feat: Add loading state to form button --- app/ui/view/molecule/form/form-submit-button.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/ui/view/molecule/form/form-submit-button.tsx b/app/ui/view/molecule/form/form-submit-button.tsx index 94c23431..1d5c54e8 100644 --- a/app/ui/view/molecule/form/form-submit-button.tsx +++ b/app/ui/view/molecule/form/form-submit-button.tsx @@ -15,7 +15,7 @@ export function FormSubmitButton({ label, position = 'right', variant = 'primary', - size = 'md', + size = 'sm', }: FormSubmitButtonProps) { const { formId } = useContext(FormContext); const { pending } = useFormStatus(); @@ -29,7 +29,7 @@ export function FormSubmitButton({ })} >