From ddbf0de8a035d374a416cd4d020ed6ea9a3f7f0a Mon Sep 17 00:00:00 2001 From: jhynsoo Date: Tue, 27 Aug 2024 14:37:28 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=AA=A8=EB=8B=AC=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?-=20S11P21S002-64?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/assets/icons/x.svg | 5 + .../ProjectCreateModal/ProjectCreateForm.tsx | 93 +++++++++ .../ProjectCreateModal/index.stories.tsx | 18 ++ .../components/ProjectCreateModal/index.tsx | 25 +++ frontend/src/components/ui/button.tsx | 59 ++++++ frontend/src/components/ui/form.tsx | 176 ++++++++++++++++++ frontend/src/components/ui/input.tsx | 25 +++ frontend/src/components/ui/label.tsx | 24 +++ frontend/src/components/ui/radio-group.tsx | 42 +++++ frontend/src/stories/Button.stories.ts | 52 ------ frontend/src/stories/Button.tsx | 48 ----- frontend/src/stories/Header.stories.ts | 33 ---- frontend/src/stories/Header.tsx | 56 ------ frontend/src/stories/Page.stories.ts | 32 ---- frontend/src/stories/Page.tsx | 73 -------- frontend/src/stories/button.css | 30 --- frontend/src/stories/header.css | 32 ---- frontend/src/stories/page.css | 69 ------- 18 files changed, 467 insertions(+), 425 deletions(-) create mode 100644 frontend/src/assets/icons/x.svg create mode 100644 frontend/src/components/ProjectCreateModal/ProjectCreateForm.tsx create mode 100644 frontend/src/components/ProjectCreateModal/index.stories.tsx create mode 100644 frontend/src/components/ProjectCreateModal/index.tsx create mode 100644 frontend/src/components/ui/button.tsx create mode 100644 frontend/src/components/ui/form.tsx create mode 100644 frontend/src/components/ui/input.tsx create mode 100644 frontend/src/components/ui/label.tsx create mode 100644 frontend/src/components/ui/radio-group.tsx delete mode 100644 frontend/src/stories/Button.stories.ts delete mode 100644 frontend/src/stories/Button.tsx delete mode 100644 frontend/src/stories/Header.stories.ts delete mode 100644 frontend/src/stories/Header.tsx delete mode 100644 frontend/src/stories/Page.stories.ts delete mode 100644 frontend/src/stories/Page.tsx delete mode 100644 frontend/src/stories/button.css delete mode 100644 frontend/src/stories/header.css delete mode 100644 frontend/src/stories/page.css diff --git a/frontend/src/assets/icons/x.svg b/frontend/src/assets/icons/x.svg new file mode 100644 index 0000000..cd6e63b --- /dev/null +++ b/frontend/src/assets/icons/x.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/components/ProjectCreateModal/ProjectCreateForm.tsx b/frontend/src/components/ProjectCreateModal/ProjectCreateForm.tsx new file mode 100644 index 0000000..458b051 --- /dev/null +++ b/frontend/src/components/ProjectCreateModal/ProjectCreateForm.tsx @@ -0,0 +1,93 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../ui/form'; +import { Input } from '../ui/input'; +import { Button } from '../ui/button'; +import { RadioGroup, RadioGroupItem } from '../ui/radio-group'; + +const formSchema = z.object({ + projectName: z.string().max(50).min(1, { + message: '이름을 입력해주세요.', + }), + labelType: z.enum(['Classification', 'Detection', 'Segmentation']), +}); + +export type ProjectCreateFormValues = z.infer; + +const defaultValues: Partial = { + projectName: '', + labelType: 'Classification', +}; + +export default function ProjectCreateForm({ onSubmit }: { onSubmit: (data: ProjectCreateFormValues) => void }) { + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues, + }); + + return ( +
+ + ( + + 이름 + + + + + + )} + /> + ( + + 레이블 종류 + + {['Classification', 'Detection', 'Segmentation'].map((labelType) => ( + + + + + + {labelType} + + + ))} + + + + )} + /> + + + + ); +} diff --git a/frontend/src/components/ProjectCreateModal/index.stories.tsx b/frontend/src/components/ProjectCreateModal/index.stories.tsx new file mode 100644 index 0000000..5af2800 --- /dev/null +++ b/frontend/src/components/ProjectCreateModal/index.stories.tsx @@ -0,0 +1,18 @@ +import '@/index.css'; +import ProjectCreateModal from '.'; + +export default { + title: 'Modal/ProjectCreateModal', + component: ProjectCreateModal, +}; + +export const Default = () => ( + { + console.log('close'); + }} + onSubmit={(data) => { + console.log(data); + }} + /> +); diff --git a/frontend/src/components/ProjectCreateModal/index.tsx b/frontend/src/components/ProjectCreateModal/index.tsx new file mode 100644 index 0000000..fdc589a --- /dev/null +++ b/frontend/src/components/ProjectCreateModal/index.tsx @@ -0,0 +1,25 @@ +import ProjectCreateForm, { ProjectCreateFormValues } from './ProjectCreateForm'; +import XIcon from '@/assets/icons/x.svg?react'; + +export default function ProjectCreateModal({ + onClose, + onSubmit, +}: { + onClose: () => void; + onSubmit: (data: ProjectCreateFormValues) => void; +}) { + return ( +
+
+

새 프로젝트

+ +
+ +
+ ); +} diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx new file mode 100644 index 0000000..c2fe07a --- /dev/null +++ b/frontend/src/components/ui/button.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '@/lib/utils'; + +const buttonVariants = cva( + 'body-strong inline-flex items-center justify-center whitespace-nowrap rounded-lg ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-950 focus-visible:ring-offset-2 disabled:pointer-events-none dark:ring-offset-gray-950 dark:focus-visible:ring-gray-300', + { + variants: { + variant: { + default: + 'bg-gray-900 text-gray-50 hover:bg-gray-900/90 dark:bg-gray-50 dark:text-gray-900 dark:hover:bg-gray-50/90', + destructive: + 'bg-red-500 text-gray-50 hover:bg-red-500/90 dark:bg-red-900 dark:text-gray-50 dark:hover:bg-red-900/90', + outline: + 'border border-gray-200 bg-white hover:bg-gray-100 hover:text-gray-900 dark:border-gray-800 dark:bg-gray-950 dark:hover:bg-gray-800 dark:hover:text-gray-50', + outlinePrimary: + 'border border-primary text-primary hover:bg-primary hover:text-white dark:border-primary dark:text-primary dark:hover:bg-primary dark:hover:text-white disabled:border-gray-200 disabled:bg-white disabled:text-gray-500 disabled:hover:bg-gray-100 disabled:hover:text-gray-300', + secondary: + 'bg-gray-100 text-gray-900 hover:bg-gray-100/80 dark:bg-gray-800 dark:text-gray-50 dark:hover:bg-gray-800/80', + ghost: 'hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-800 dark:hover:text-gray-50', + link: 'text-gray-900 underline-offset-4 hover:underline dark:text-gray-50', + }, + size: { + default: 'h-10 px-4 py-2', + sm: 'h-9 rounded-md px-3', + lg: 'h-11 rounded-md px-8', + icon: 'h-10 w-10', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button'; + return ( + + ); + } +); +Button.displayName = 'Button'; + +export { Button, buttonVariants }; diff --git a/frontend/src/components/ui/form.tsx b/frontend/src/components/ui/form.tsx new file mode 100644 index 0000000..4c946cf --- /dev/null +++ b/frontend/src/components/ui/form.tsx @@ -0,0 +1,176 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +