Skip to content

Commit

Permalink
Merge branch 'develop' of https://github.com/GU-99/grow-up-fe into fe…
Browse files Browse the repository at this point in the history
…ature/#123-logout
  • Loading branch information
Yoonyesol committed Sep 15, 2024
2 parents c886f4c + fae8a00 commit 0fcf10a
Show file tree
Hide file tree
Showing 16 changed files with 465 additions and 259 deletions.
27 changes: 27 additions & 0 deletions src/components/common/AssigneeList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { IoMdCloseCircle } from 'react-icons/io';
import RoleIcon from '@components/common/RoleIcon';
import type { UserWithRole } from '@/types/UserType';

type AssigneeListProps = {
assigneeList: UserWithRole[];
onAssigneeDeleteClick: (user: UserWithRole) => void;
};

export default function AssigneeList({
assigneeList,
onAssigneeDeleteClick: handleWorkerDeleteClick,
}: AssigneeListProps) {
return (
<section className="flex w-full flex-wrap items-center gap-4">
{assigneeList.map((user) => (
<div key={user.userId} className="flex items-center space-x-4 rounded-md bg-button px-5">
<RoleIcon roleName={user.roleName} />
<div>{user.nickname}</div>
<button type="button" aria-label="delete-worker" onClick={() => handleWorkerDeleteClick(user)}>
<IoMdCloseCircle className="text-close" />
</button>
</div>
))}
</section>
);
}
2 changes: 1 addition & 1 deletion src/components/common/CustomMarkdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export default function CustomMarkdown({ markdown }: CustomMarkdownProps) {
const changedMarkdown = useMemo(() => getChangedMarkdownForLineBreak(markdown), [markdown]);

return (
<section className="rounded-md border border-input p-10 text-sm">
<section className="min-h-80 rounded-md border border-input p-10 text-sm">
{markdown.trim().length === 0 ? (
<div className="text-xs text-gray-400/90">입력된 내용이 없습니다.</div>
) : (
Expand Down
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>
);
}
73 changes: 73 additions & 0 deletions src/components/common/SearchUserInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { IoSearch } from 'react-icons/io5';
import type { UserWithRole } from '@/types/UserType';

type SearchInputProps = {
id: string;
label: string;
keyword: string;
loading: boolean;
userList: UserWithRole[];
onKeywordChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
onSearchKeyup: (event: React.KeyboardEvent<HTMLInputElement>) => void;
onSearchClick: () => void;
onUserClick: (user: UserWithRole) => void;
};

export default function SearchUserInput({
id,
label,
keyword,
loading,
userList,
onKeywordChange: handleKeywordChange,
onSearchKeyup: handleSearchKeyUp,
onSearchClick: handleSearchClick,
onUserClick: handleUserClick,
}: SearchInputProps) {
return (
<label htmlFor={id} className="group mb-10 flex items-center gap-5">
<h3 className="text-large">{label}</h3>
<section className="relative grow">
<input
type="text"
id={id}
className="h-25 w-full rounded-md border border-input pl-10 pr-25 text-regular placeholder:text-xs"
value={keyword}
onChange={handleKeywordChange}
onKeyDown={handleSearchKeyUp}
placeholder="닉네임을 검색해주세요."
/>
<button
type="button"
aria-label="search"
className="absolute right-5 top-1/2 -translate-y-1/2 cursor-pointer"
onClick={handleSearchClick}
>
<IoSearch className="size-15 text-emphasis hover:text-black" />
</button>
{keyword && !loading && (
<ul className="invisible absolute left-0 right-0 z-10 max-h-110 overflow-auto rounded-md border-2 bg-white group-focus-within:visible">
{userList.length === 0 ? (
<div className="h-20 border px-10 leading-8">&apos;{keyword}&apos; 의 검색 결과가 없습니다.</div>
) : (
userList?.map((user) => (
<li className="h-20 border" key={user.userId}>
<button
type="button"
className="h-full w-full px-10 text-left hover:bg-sub"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.currentTarget.blur();
handleUserClick(user);
}}
>
{user.nickname}
</button>
</li>
))
)}
</ul>
)}
</section>
</label>
);
}
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>
</>
);
}
2 changes: 1 addition & 1 deletion src/components/modal/ModalFormButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ type ModalFormButtonProps = {
export default function ModalFormButton({ formId, isCreate, onClose }: ModalFormButtonProps) {
return (
<div className="min-h-25 w-4/5">
<button type="submit" form={formId} className="mr-10 h-full w-full rounded-md bg-main px-10 text-white">
<button type="submit" form={formId} className="h-full w-full rounded-md bg-main px-10 text-white">
{isCreate ? '등록' : '수정'}
</button>
</div>
Expand Down
Loading

0 comments on commit 0fcf10a

Please sign in to comment.