Skip to content

Commit

Permalink
UI: #127 일정 등록 입력 양식들을 컴포넌트로 분리 (#131)
Browse files Browse the repository at this point in the history
* UI: #127 프로젝트 상태 radio input 컴포넌트 분리

* UI: #127 시작일, 종료일 컴포넌트 분리

* UI: #127 마크다운 컴포넌트 분리

* UI: #127 파일 DropZone 컴포넌트 분리

* UI: #127 마크다운 컴포넌트 props 변경

* UI: #127 시작일, 종료일 컴포넌트 props 변경

* UI: #127 프로젝트 상태 컴포넌트 props 변경

* UI: #127 시작일, 종료일 컴포넌트 props 변경

* UI: #127 마크다운 컴포넌트 props 변경
  • Loading branch information
Seok93 authored Sep 14, 2024
1 parent a4579b8 commit 536d70b
Show file tree
Hide file tree
Showing 6 changed files with 335 additions and 206 deletions.
51 changes: 51 additions & 0 deletions src/components/common/FileDropZone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { GoPlusCircle } from 'react-icons/go';
import { IoMdCloseCircle } from 'react-icons/io';
import type { CustomFile } from '@/types/FileType';

type FileDropZoneProps = {
id: string;
label: string;
files: CustomFile[];
onFileChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onFileDrop: (e: React.DragEvent<HTMLElement>) => void;
onFileDeleteClick: (fileId: string) => void;
};

// ToDo: 파일 업로드 API 작업시 구조 다시 한 번 확인해보기
export default function FileDropZone({
id,
label,
files,
onFileChange: handleFileChange,
onFileDrop: handleFileDrop,
onFileDeleteClick: handleFileDeleteClick,
}: FileDropZoneProps) {
return (
<label htmlFor={id}>
<h3 className="text-large">{label}</h3>
<input type="file" id={id} className="h-0 w-0 opacity-0" multiple hidden onChange={handleFileChange} />
<section
className="flex cursor-pointer items-center gap-4 rounded-sl border-2 border-dashed border-input p-10"
onDrop={handleFileDrop}
>
<ul className="flex grow flex-wrap gap-4">
{files.map(({ id, file }) => (
<li key={id} className="flex items-center gap-4 rounded-md bg-button px-4 py-2">
<span>{file.name}</span>
<IoMdCloseCircle
className="text-close"
onClick={(e: React.MouseEvent<HTMLOrSVGElement>) => {
e.preventDefault();
handleFileDeleteClick(id);
}}
/>
</li>
))}
</ul>
<div>
<GoPlusCircle className="size-15 text-[#5E5E5E]" />
</div>
</section>
</label>
);
}
37 changes: 37 additions & 0 deletions src/components/common/MarkdownEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useState } from 'react';
import { useFormContext } from 'react-hook-form';
import ToggleButton from '@components/common/ToggleButton';
import CustomMarkdown from '@components/common/CustomMarkdown';

type MarkdownEditorProps = {
id: string;
label: string;
contentFieldName: string;
};

export default function MarkdownEditor({ id, label, contentFieldName }: MarkdownEditorProps) {
const [preview, setPreview] = useState(false);
const { watch, register } = useFormContext();

const handlePreviewToggle = () => setPreview((prev) => !prev);

return (
<label htmlFor={id} className="mb-20">
<h3 className="flex items-center space-x-2">
<span className="text-large">{label}</span>
<ToggleButton id="preview" checked={preview} onChange={handlePreviewToggle} />
</h3>
{preview ? (
<CustomMarkdown markdown={watch(contentFieldName)} />
) : (
<textarea
id={id}
rows={10}
className="w-full border border-input p-10 placeholder:text-xs"
placeholder="마크다운 형식으로 입력해주세요."
{...register(contentFieldName)}
/>
)}
</label>
);
}
85 changes: 85 additions & 0 deletions src/components/common/PeriodDateInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { TASK_VALIDATION_RULES } from '@constants/formValidationRules';
import ToggleButton from '@components/common/ToggleButton';

import type { FieldError } from 'react-hook-form';
import type { Project } from '@/types/ProjectType';

type PeriodDateInputProps = {
startDateLabel: string;
endDateLabel: string;
startDateId: string;
endDateId: string;
startDate: Project['startDate'];
endDate: Project['endDate'];
startDateFieldName: string;
endDateFieldName: string;
};

export default function PeriodDateInput({
startDateLabel,
endDateLabel,
startDateId,
endDateId,
startDate,
endDate,
startDateFieldName,
endDateFieldName,
}: PeriodDateInputProps) {
const [hasDeadline, setHasDeadline] = useState(false);
const {
setValue,
getValues,
clearErrors,
watch,
register,
formState: { errors },
} = useFormContext();

const handleDeadlineToggle = () => {
setValue(endDateFieldName, getValues(startDateFieldName));
clearErrors(endDateFieldName);
setHasDeadline((prev) => !prev);
};

return (
<div className="flex items-center justify-center gap-10">
<label htmlFor={startDateId} className="w-1/2">
<h3 className="text-large">{startDateLabel}</h3>
<input
id={startDateId}
type="date"
{...register(startDateFieldName, {
...TASK_VALIDATION_RULES.START_DATE(startDate, endDate),
onChange: (e) => {
if (!hasDeadline) setValue(endDateFieldName, e.target.value);
},
})}
/>
<div className={`my-5 h-10 grow text-xs text-error ${errors[startDateFieldName] ? 'visible' : 'invisible'}`}>
{(errors[startDateFieldName] as FieldError | undefined)?.message}
</div>
</label>
<label htmlFor={endDateId} className="w-1/2">
<h3 className="flex items-center space-x-2 text-large">
<span>{endDateLabel}</span>
<ToggleButton id="deadline" checked={hasDeadline} onChange={handleDeadlineToggle} />
</h3>
<input
id={endDateId}
type="date"
className={`${hasDeadline ? '' : '!bg-disable'}`}
disabled={!hasDeadline}
{...register(
endDateFieldName,
TASK_VALIDATION_RULES.END_DATE(hasDeadline, startDate, endDate, watch(startDateFieldName)),
)}
/>
<div className={`my-5 h-10 grow text-xs text-error ${errors[endDateFieldName] ? 'visible' : 'invisible'}`}>
{(errors[endDateFieldName] as FieldError | undefined)?.message}
</div>
</label>
</div>
);
}
51 changes: 51 additions & 0 deletions src/components/common/StatusRadio.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useFormContext } from 'react-hook-form';
import { TASK_VALIDATION_RULES } from '@constants/formValidationRules';

import type { FieldError } from 'react-hook-form';
import type { ProjectStatus } from '@/types/ProjectStatusType';

type StatusRadioProps = {
statusFieldName: string;
statusList: ProjectStatus[];
};

export default function StatusRadio({ statusFieldName, statusList }: StatusRadioProps) {
const {
watch,
register,
formState: { errors },
} = useFormContext();

return (
<>
{/* ToDo: 상태 선택 리팩토링 할 것 */}
<div className="flex items-center justify-start gap-4">
{statusList.map((status) => {
const { statusId, statusName, colorCode } = status;
const isChecked = Number(watch('statusId')) === statusId;
return (
<label
key={statusId}
htmlFor={statusName}
className={`flex cursor-pointer items-center rounded-lg border px-5 py-3 text-emphasis ${isChecked ? 'border-input bg-white' : 'bg-button'}`}
>
<input
id={statusName}
type="radio"
className="invisible h-0 w-0"
value={statusId}
checked={isChecked}
{...register(statusFieldName, TASK_VALIDATION_RULES.STATUS)}
/>
<div style={{ borderColor: colorCode }} className="mr-3 h-8 w-8 rounded-full border" />
<h3 className="text-xs">{statusName}</h3>
</label>
);
})}
</div>
<div className={`my-5 h-10 grow text-xs text-error ${errors[statusFieldName] ? 'visible' : 'invisible'}`}>
{(errors[statusFieldName] as FieldError | undefined)?.message}
</div>
</>
);
}
Loading

0 comments on commit 536d70b

Please sign in to comment.