Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(client): 프로젝트 생성 페이지 퍼블리싱 #37

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
5 changes: 5 additions & 0 deletions apps/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,19 @@
"devDependencies": {
"@jjoing/eslint-config": "workspace:*",
"@jjoing/typescript-config": "workspace:*",
"@toss/use-overlay": "^1.4.0",
"@types/node": "^20.8.0",
"@types/react": "^18.2.23",
"@types/react-dom": "^18.2.7",
"autoprefixer": "^10.4.14",
"dayjs": "^1.11.13",
"eslint": "^8.50.0",
"eslint-config-next": "^14.0.5",
"framer-motion": "^11.3.30",
"postcss": "^8.4.27",
"react-dropzone": "^14.2.3",
"react-hook-form": "^7.53.0",
"tailwind-scrollbar-hide": "^1.1.7",
"tailwindcss": "^3.3.3",
"typescript": "^5.2.2"
}
Expand Down
19 changes: 15 additions & 4 deletions apps/client/src/app/projects/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
'use client';

import CreateProject from '@/components/createProject';
import { Container, Wrapper } from '@/components/layouts';
import { ProjectList, ProjectSelectBox } from '@/components/projects';
import { ProjectRecruitOptions, ProjectRecruitStatus } from '@/constants';
import { useOverlay } from '@toss/use-overlay';
import { useState } from 'react';
import { FaPlus } from 'react-icons/fa6';

const ProjectsPage = () => {
const [projectStatus, setProjectStatus] = useState(ProjectRecruitStatus[0]?.state);
const [projectOptions, setProjectOpoins] = useState(ProjectRecruitOptions[0]?.state);
const [projectOptions, setProjectOptions] = useState(ProjectRecruitOptions[0]?.state);

console.log(projectStatus, projectOptions); // 백엔드로 요청보낼때 사용될 코드
console.log(projectStatus, projectOptions); // 백엔드로 요청보낼때 사용될 코드s

const overlay = useOverlay();

const handleOpenCreateProject = () => {
overlay.open(({ isOpen, close }) => <CreateProject open={isOpen} close={close} />);
};

return (
<Container className="py-10 min-h-dvh bg-gray-10">
Expand All @@ -22,12 +30,15 @@ const ProjectsPage = () => {
/>
<ProjectSelectBox
options={ProjectRecruitOptions}
setSelectedSortOption={setProjectOpoins}
setSelectedSortOption={setProjectOptions}
/>
</div>
<div className="flex items-end justify-between">
<span className="text-xl font-medium">프로젝트 목록 📋</span>
<div className="flex items-center justify-center rounded-md bg-primary size-[35px] cursor-p hover:bg-primaryHover transition duration-15">
<div
onClick={handleOpenCreateProject}
className="flex items-center justify-center rounded-md bg-primary size-[35px] cursor-pointer hover:bg-primaryHover transition duration-150"
>
<FaPlus className="size-[18px] text-white" />
</div>
</div>
Expand Down
14 changes: 14 additions & 0 deletions apps/client/src/components/createProject/explainField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Text, Textarea } from '@jjoing/ui';
import { useFormContext } from 'react-hook-form';

const ExplainField = () => {
const { register } = useFormContext();
return (
<div className="flex flex-col gap-1 mb-4">
<Text type="body2">프로젝트 설명</Text>
<Textarea placeholder="프로젝트를 설명해주세요." {...register('projectExplain')} />
</div>
);
};

export default ExplainField;
43 changes: 43 additions & 0 deletions apps/client/src/components/createProject/firstCreateProjectBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Button } from '@jjoing/ui';
import ExplainField from './explainField';
import FormArrayField from './formArrayField';
import FormField from './formField';
import RecruitPeriodField from './recruitPeriodField';

type FirstCreateProjectBoxProps = {
setPage: React.Dispatch<React.SetStateAction<number>>;
};

const FirstCreateProjectBox = ({ setPage }: FirstCreateProjectBoxProps) => {
const handleNextPage = () => setPage(1);

return (
<>
<FormField
title="프로젝트 이름"
placeholder="프로젝트 이름을 알려주세요."
fieldName="projectName"
/>
<FormField
title="모집 인원"
placeholder="모집 인원을 알려주세요."
fieldName="recruitMember"
type="number"
/>
<RecruitPeriodField />
<FormArrayField
title="모집 분야"
placeholder="예시) 디자이너, 백엔드 (엔터로 구분해 주세요!)"
fieldName="projectField"
/>
<ExplainField />
<div className="flex justify-end">
<Button height="h45" width={100} onClick={handleNextPage}>
다음
</Button>
</div>
</>
);
};

export default FirstCreateProjectBox;
53 changes: 53 additions & 0 deletions apps/client/src/components/createProject/formArrayField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Button, Input, Text } from '@jjoing/ui';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { IoClose } from 'react-icons/io5';

type FormFieldProps = {
title: string;
placeholder: string;
fieldName: string;
};

const FormArrayField = ({ title, placeholder, fieldName }: FormFieldProps) => {
const { control } = useFormContext();

const { fields, append, remove } = useFieldArray({
control,
name: fieldName,
});

const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && !e.nativeEvent.isComposing) {
e.preventDefault();

const newValue = e.currentTarget.value;

append({ value: newValue });
e.currentTarget.value = '';
}
};

return (
<div className="flex flex-col gap-1 mb-3">
<Text type="body2">{title}</Text>
<Input width="100%" onKeyDown={handleKeyDown} placeholder={placeholder} />
<div className="mt-2 flex gap-1 overflow-x-auto whitespace-nowrap scrollbar-hide">
{fields.map((item: any, index: number) => (
<Button
bgColor="borderGray"
rounded="full"
height="h40"
className="px-4 flex items-center"
style={{ width: 'auto !important' }} // Button 기본 넓이가 100%라서 강제로 넓이 스타일 적용해줬음 (120px 이런식으로 넣으면 auto가 안됨)
key={item.id}
>
{item.value}
<IoClose onClick={() => remove(index)} className="ml-1 cursor-pointer" />
</Button>
))}
</div>
</div>
);
};

export default FormArrayField;
30 changes: 30 additions & 0 deletions apps/client/src/components/createProject/formField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Input, Text } from '@jjoing/ui';
import { useFormContext } from 'react-hook-form';

type FormFieldProps = {
title: string;
placeholder: string;
fieldName: string;
type?: string;
};

const FormField = ({ title, placeholder, fieldName, type = 'text' }: FormFieldProps) => {
const { register } = useFormContext();

return (
<div className="flex flex-col gap-1 mb-4">
<Text type="body2">{title}</Text>
<Input
type={type}
width="100%"
placeholder={placeholder}
defaultValue={type === 'number' ? 1 : undefined}
min={1}
max={10}
{...register(fieldName)}
/>
</div>
);
};

export default FormField;
60 changes: 60 additions & 0 deletions apps/client/src/components/createProject/imageField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Text } from '@jjoing/ui';
import { useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { useFormContext } from 'react-hook-form';
import { IoFolderOpenOutline } from 'react-icons/io5';

const ImageField = () => {
const { register } = useFormContext();
const [previewImage, setPreviewImage] = useState('');

const encodingImageUrl = (file: File | null) => {
if (file) {
const imageUrl = URL.createObjectURL(file);
setPreviewImage(imageUrl);
}
};

const onDrop = (dragImage?: File[]) => {
const imageFile = dragImage?.[0] ?? null;
encodingImageUrl(imageFile);
};

const { getRootProps, getInputProps } = useDropzone({ onDrop });

const handleChangeImage = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0] ?? null;
encodingImageUrl(file);
};

return (
<div className="flex flex-col gap-1 mb-4">
<Text type="body2">모집 기한</Text>
<div
{...getRootProps()}
className="bg-gray-50 w-full h-60 dashed-border rounded-lg flex items-center justify-center relative bg-cover bg-center"
style={{ backgroundImage: previewImage && `url(${previewImage})` }}
>
<input
type="file"
accept="image/*"
{...register('image', {
onChange: handleChangeImage,
})}
{...getInputProps()}
className="size-full absolute opacity-0 cursor-pointer"
/>
{!previewImage && (
<div className="flex flex-col gap-3 justify-center items-center">
<IoFolderOpenOutline className="text-gray-300 size-16" />
<Text className="text-gray-300">
이미지를 드래그 앤 드롭 또는 직접 업로드를 해주세요.
</Text>
</div>
)}
</div>
</div>
);
};

export default ImageField;
54 changes: 54 additions & 0 deletions apps/client/src/components/createProject/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { OverlayModal } from '@/types';
import { Text } from '@jjoing/ui';
import { useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { IoClose } from 'react-icons/io5';
import ModalWrapper from '../layouts/modalWrapper';
import FirstCreateProjectBox from './firstCreateProjectBox';
import SecondCreateProjectBox from './secondCreateProjectBox';

type CreateProjectForm = {
projectName: string;
recruitMember: number;
startDate: string;
endDate: string;
projectField: string[];
projectExplain: string;

mood: string[];
developSkills: string[];
developTools: string[];
image: string;
};

const CreateProject = ({ open, close }: OverlayModal) => {
const [page, setPage] = useState(0);

const methods = useForm<CreateProjectForm>();

const onSubmit = (data: CreateProjectForm) => {
console.log('data: ', data);
};

return (
<ModalWrapper open={open} close={close}>
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<div className="bg-white size-full p-7 rounded-xl">
<div className="flex items-center justify-between mb-5">
<Text type="body1">프로젝트 생성하기 🖨</Text>
<IoClose onClick={close} className="size-7 cursor-pointer" />
</div>
{page === 0 ? (
<FirstCreateProjectBox setPage={setPage} />
) : (
<SecondCreateProjectBox setPage={setPage} />
)}
</div>
</form>
</FormProvider>
</ModalWrapper>
);
};

export default CreateProject;
31 changes: 31 additions & 0 deletions apps/client/src/components/createProject/recruitPeriodField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Input, Text } from '@jjoing/ui';
import dayjs from 'dayjs';
import { useFormContext } from 'react-hook-form';

const RecruitPeriodField = () => {
const { register } = useFormContext();
const today = dayjs().format('YYYY. MM. DD.');

return (
<div className="flex flex-col gap-1 mb-4">
<Text type="body2">모집 기한</Text>
<div className="flex gap-2">
<Input
width="100%"
value={today}
placeholder="모집 시작 시간을 알려주세요."
{...register('startDate')}
readOnly
/>
<Input
width="100%"
placeholder="모집 종료 시간을 알려주세요."
type="date"
{...register('endDate')}
/>
</div>
</div>
);
};

export default RecruitPeriodField;
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Button } from '@jjoing/ui';
import FormArrayField from './formArrayField';
import ImageField from './imageField';

type SecondCreateProjectBoxProps = {
setPage: React.Dispatch<React.SetStateAction<number>>;
};

const SecondCreateProjectBox = ({ setPage }: SecondCreateProjectBoxProps) => {
const handlePrevPage = () => setPage(0);

return (
<>
<FormArrayField
title="분위기 유형"
placeholder="예시) 묵묵한, 충실한 (엔터로 구분해 주세요!)"
fieldName="mood"
/>
<FormArrayField
title="사용 기술"
placeholder="예시) 리액트, 노드 (엔터로 구분해 주세요!)"
fieldName="developSkills"
/>
<FormArrayField
title="협업 툴"
placeholder="예시) vsCode, intellij (엔터로 구분해 주세요!)"
fieldName="developTools"
/>
<ImageField />
<div className="flex justify-between">
<Button height="h45" width={100} bgColor="gray" onClick={handlePrevPage}>
이전
</Button>
<Button height="h45" width={100}>
제출
</Button>
</div>
</>
);
};

export default SecondCreateProjectBox;
Loading
Loading