Skip to content

Commit

Permalink
Feature/#230 대회 등록 페이지 구현 (#231)
Browse files Browse the repository at this point in the history
* design: 마일스톤 페이지의 header 통일

#230

* feat: 대회 등록 페이지 header 구현 및 type 선언

#230

* refactor: input요소 통일

#230

* feat: 파일 업로더 컴포넌트에서 file의 타입 undefind -> null 로 설정

#230

* chore: react markdown editor 라이브러리 추가

#230

* feat: 관리자페이지의 대회 등록에 공통으로 사용될 input ui 구현

#230

* feat: formik과 호환되는 마크다운 에디터 컴포넌트 구현

#230

* refactor: formik 커스텀 인풋 컴포넌트에서 필요없는 속성 삭제

#230

* feat: 관리자 페이지의 대회 등록 페이지 구현

#230

* chore: 관리자 페이지의 페이지 카테고리 변경

#230

* fix: 빌드 오류 해결

#230
  • Loading branch information
llddang authored Dec 8, 2024
1 parent 4a16487 commit 50c8cfd
Show file tree
Hide file tree
Showing 25 changed files with 2,269 additions and 240 deletions.
1,933 changes: 1,925 additions & 8 deletions frontend/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@tiptap/pm": "^2.6.4",
"@tiptap/react": "^2.6.4",
"@tiptap/starter-kit": "^2.6.4",
"@uiw/react-md-editor": "^4.0.4",
"axios": "^1.7.2",
"babel-plugin-styled-components": "^2.1.4",
"classnames": "^2.5.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const validationSchema = Yup.object().shape({
});

interface HackathonTeamFormProps {
image?: File;
image: File | null;
name: string;
work: string;
githubUrl: string;
Expand All @@ -65,7 +65,7 @@ interface HackathonTeamFormProps {
}

const initialValues: HackathonTeamFormProps = {
image: undefined,
image: null,
name: '',
work: '',
githubUrl: '',
Expand Down Expand Up @@ -144,7 +144,6 @@ const HackathonTeamCreateModal = ({ hackathonId, open, onClose }: HackathonTeamC
<div className="mx-auto h-[120px] w-[210px] md:m-0">
<ImageUploader
name="image"
label=""
image={values.image}
setFieldValue={setFieldValue}
errorText={touched.image && errors.image ? errors.image : undefined}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/(client)/my-page/milestone-list/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default async function MilestoneHistoryListPage({ searchParams }: Milesto
return (
<div className="rounded-sm bg-white p-5">
<div className="mb-10 flex items-center justify-between">
<PageTitle title="실적 등록" description="나의 마일스톤 실적 결과 등록" />
<PageTitle title="마일스톤 등록 내역" description="나의 마일스톤 실적 등록 내역" />
<Link href="/my-page/milestone-register" className="rounded-sm bg-primary-main px-5 py-1 text-white">
실적 등록
</Link>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,7 @@ export default function MilestoneRegisterPage() {

return (
<div className="rounded-sm bg-white p-5">
<PageTitle title="실적 등록" />
<p className="mb-10 mt-6 flex items-center justify-between border-b border-black py-4 text-lg font-bold">
실적 등록하기
</p>
<PageTitle title="마일스톤 등록" className="pb-8" />
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
Expand Down Expand Up @@ -145,7 +142,6 @@ export default function MilestoneRegisterPage() {
errorText={touched.description && errors.description ? errors.description : undefined}
/>
<DatePicker
type="date"
name="activatedAt"
label="활동 인정일"
tooltip="마일스톤 실적이 공식적으로 인정된 날짜를 선택해주세요.\n ex) 대회 수상일, 자격증 취득일, 프로젝트 완료일."
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/app/(client)/my-page/milestone/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import MyPageMilestoneOverview from '@/components/ui/my-page/MyPageMilestoneOver
import MilestoneAcceptedTable from '@/components/ui/milestone/MilestoneAcceptedTable';

import { Period } from '@/types/common';
import PageTitle from '@/components/common/PageTitle';

export default function MyPageMilestonePage() {
const searchParams = useSearchParams();
Expand All @@ -29,7 +30,7 @@ export default function MyPageMilestonePage() {
return (
<div className="w-full rounded-sm bg-white p-5">
<div className="mb-6 flex flex-wrap justify-between gap-4">
<p className="min-w-[10em] text-xl font-bold">마일스톤 획득 내역</p>
<PageTitle title="마일스톤 획득 내역" className="grow" />
<PeriodSearchBox setPeriod={setFilterPeriod} period={filterPeriod} setSearchPeriod={setSearchFilterPeriod} />
</div>
<div className="mb-6 text-lg font-bold">전체 현황</div>
Expand Down
7 changes: 0 additions & 7 deletions frontend/src/app/admin/contest/create/page.tsx

This file was deleted.

File renamed without changes.
194 changes: 194 additions & 0 deletions frontend/src/app/admin/hackathon/register/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
'use client';

import { useState } from 'react';
import * as Yup from 'yup';
import { Form, Formik } from 'formik';

import PageTitle from '@/components/common/PageTitle';
import { HackathonDto } from '@/types/common.dto';
import TextInput from '@/components/common/formik/TextInput';
import AdminHackathonInputSection from '@/components/ui/admin/hackathon/AdminHackathonInputSection';
import { MdImage } from '@react-icons/all-files/md/MdImage';
import ImageUploader from '@/components/common/formik/ImageUploader';
import { MdTextFields } from '@react-icons/all-files/md/MdTextFields';
import { MdDateRange } from '@react-icons/all-files/md/MdDateRange';
import MarkdownEditor from '@/components/common/formik/MarkdownEditor';
import { DatePicker } from '@/components/common/formik/DatePicker';

export default function HackathonCreatePage() {
const [hackathonInfo, setHackathonInfo] = useState<HackathonInfo>(hackathonInfoInitialValue);

return (
<div className="w-full">
<PageTitle title="해커톤 등록" />
<Formik
initialValues={hackathonInfo}
validationSchema={hackathonValidationSchema}
onSubmit={(values, { setSubmitting }) => {
setSubmitting(false);
// TODO: API 연결
}}
>
{({ isSubmitting, values, touched, handleChange, handleBlur, setFieldValue, errors }) => (
<Form className="m-5 flex flex-col gap-6" autoComplete="off">
<AdminHackathonInputSection
icon={MdTextFields}
label="대회명"
inputElement={
<TextInput
name="name"
value={values.name}
onChange={handleChange}
onBlur={handleBlur}
errorText={touched.name && errors.name ? errors.name : undefined}
placeholder="대회명을 입력해주세요."
/>
}
/>
<AdminHackathonInputSection
icon={MdImage}
label="배너 이미지"
inputElement={
<div className="h-[120px] w-full">
<ImageUploader
name="bannerImage"
image={values.bannerImage}
fitStand="height"
setFieldValue={setFieldValue}
errorText={touched.bannerImage && errors.bannerImage ? errors.bannerImage : undefined}
/>
</div>
}
/>
<AdminHackathonInputSection
icon={MdTextFields}
label="대회명"
inputElement={
<MarkdownEditor
name="content"
value={values.content}
height="400px"
onChange={handleChange}
onBlur={handleBlur}
errorText={touched.content && errors.content ? errors.content : undefined}
/>
}
/>
<AdminHackathonInputSection
icon={MdDateRange}
label="신청 기간"
inputElement={
<div className="flex items-center justify-center gap-4">
<DatePicker
style={{ width: '200px' }}
name="applyStartDate"
value={values.applyStartDate}
onChange={handleChange}
onBlur={handleBlur}
errorText={touched.applyStartDate && errors.applyStartDate ? errors.applyStartDate : undefined}
/>
~
<DatePicker
style={{ width: '200px' }}
name="applyEndDate"
value={values.applyEndDate}
onChange={handleChange}
onBlur={handleBlur}
errorText={touched.applyEndDate && errors.applyEndDate ? errors.applyEndDate : undefined}
/>
</div>
}
/>
<AdminHackathonInputSection
icon={MdDateRange}
label="대회 기간"
inputElement={
<div className="flex items-center justify-center gap-4">
<DatePicker
style={{ width: '200px' }}
name="hackathonStartDate"
value={values.hackathonStartDate}
onChange={handleChange}
onBlur={handleBlur}
errorText={
touched.hackathonStartDate && errors.hackathonStartDate ? errors.hackathonStartDate : undefined
}
/>
~
<DatePicker
style={{ width: '200px' }}
name="hackathonEndDate"
value={values.hackathonEndDate}
onChange={handleChange}
onBlur={handleBlur}
errorText={
touched.hackathonEndDate && errors.hackathonEndDate ? errors.hackathonEndDate : undefined
}
/>
</div>
}
/>
<AdminHackathonInputSection
icon={MdTextFields}
label="팀 등록 코드"
tooltip="대회에 팀을 등록할 때 필요한 비밀번호입니다."
inputElement={
<TextInput
style={{ width: '441px' }}
name="teamCode"
value={values.teamCode}
onChange={handleChange}
errorText={touched.teamCode && errors.teamCode ? errors.teamCode : undefined}
placeholder="팀 등록 코드"
/>
}
/>
<div className="pt-4">
<button
type="submit"
className="rounded-sm bg-admin-primary-main px-8 py-2 text-lg font-bold text-white transition-colors hover:bg-admin-primary-dark"
disabled={isSubmitting}
>
등록하기
</button>
</div>
</Form>
)}
</Formik>
</div>
);
}

export type HackathonInfo = Omit<HackathonDto, 'bannerImage' | 'id'> & { bannerImage: File | null };
const hackathonInfoInitialValue: HackathonInfo = {
name: '',
content: '',
bannerImage: null,
applyStartDate: '',
applyEndDate: '',
hackathonStartDate: '',
hackathonEndDate: '',
teamCode: '',
};

const hackathonValidationSchema = Yup.object().shape({
name: Yup.string().max(15, '대회명을 50자 이내로 입력해주세요.').required('등록할 대회명을 입력해주세요.'),
content: Yup.string().required('등록할 대회의 상세정보를 입력해주세요.'),
bannerImage: Yup.mixed()
.required('대회의 배너 이미지를 첨부해주세요.')
.test(
'fileFormat',
'이미지 파일(.jpg, .jpeg, .png), PDF 파일(.pdf)만 업로드 가능합니다.',
(value) =>
value instanceof File && ['image/jpeg', 'image/jpg', 'image/png', 'application/pdf'].includes(value.type),
),
applyStartDate: Yup.date().required('대회 신청 시작일을 입력해주세요.'),
applyEndDate: Yup.date()
.min(Yup.ref('applyStartDate'), '신청 시작일보다 늦은 날짜로 지정해야 합니다')
.required('대회 신청 마지막 날을 입력해주세요.'),
hackathonStartDate: Yup.date().required('대회 시작일을 입력해주세요.'),
hackathonEndDate: Yup.date()
.min(Yup.ref('hackathonStartDate'), '대회 시작일보다 늦은 날짜로 지정해야 합니다')
.required('대회 마지막 일을 입력해주세요.'),
teamCode: Yup.string().required('대회에 참여할 수 있는 팀 등록 코드를 입력해주세요.'),
});
12 changes: 8 additions & 4 deletions frontend/src/components/common/PageTitle.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
export interface PageTitleProps {
export interface CustomPageTitleProps {
title: string;
description?: string;
}

export default function PageTitle({ title, description }: PageTitleProps) {
type BuiltInDivProps = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;

export type PageTitleProps = BuiltInDivProps & CustomPageTitleProps;

export default function PageTitle({ title, description, ...props }: PageTitleProps) {
return (
<div className="flex flex-col gap-1">
<p className="cursor-default text-[28px] font-semibold">{title}</p>
<div className="flex flex-col gap-1" {...props}>
<h1 className="cursor-default text-[28px] font-semibold">{title}</h1>
{description && <div className="text-comment">{description}</div>}
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/common/PeriodSearchBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default function PeriodSearchBox({ period, setPeriod, setSearchPeriod }:
};

return (
<div className="flex flex-grow flex-col items-center justify-center gap-x-4 sm:flex-row">
<div className="flex flex-col items-center justify-center gap-x-4 sm:flex-row">
<input
className="rounded-md border-none bg-border p-2 text-center focus:outline-black"
type="date"
Expand Down
34 changes: 15 additions & 19 deletions frontend/src/components/common/formik/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,36 @@ type BuiltInInputProps = React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLI

interface CustomInputProps {
name: string;
label: string;
label?: string;
isRequired?: boolean;
errorText?: string;
tooltip?: string;
onChangeText?(text: string): void;
}

export type DatePickerProps = BuiltInInputProps & CustomInputProps;

export const DatePicker = ({ isRequired = false, ...props }: DatePickerProps) => {
const { label, errorText, tooltip, onChangeText, ...inputProps } = props;
const { label, errorText, tooltip, ...inputProps } = props;
const hasError = errorText !== undefined;

return (
<div className="flex flex-col gap-1">
<label htmlFor={inputProps.id || inputProps.name} className="flex items-center text-sm font-semibold">
{tooltip && (
<div className="relative flex items-center gap-1 px-2 py-1 text-xs">
<VscInfo className="peer h-[14px] w-[14px]" />
<div className="absolute left-1/2 top-1 hidden -translate-x-1/2 -translate-y-[calc(100%+4px)] whitespace-nowrap break-keep rounded border bg-white p-2 peer-hover:block">
{tooltip.split('\\n').map((data, index) => (
<div key={`${index}-${data}`}>{data}</div>
))}
{label && (
<label htmlFor={inputProps.name} className={`flex items-center text-sm font-semibold`}>
{tooltip && (
<div className="relative flex items-center gap-1 px-2 py-1 text-xs">
<VscInfo className="peer h-[14px] w-[14px]" />
<div className="absolute left-0 top-1 hidden -translate-y-[calc(100%+4px)] whitespace-nowrap break-keep rounded border bg-white p-2 peer-hover:block">
{tooltip}
</div>
</div>
</div>
)}
{label} {isRequired && <span className="text-sm font-semibold text-red-400">*</span>}
</label>
)}
{label} {isRequired && <span className="text-xs font-semibold text-red-400">*</span>}
</label>
)}
<input
{...inputProps}
onChange={(e) => {
inputProps.onChange?.(e);
onChangeText?.(e.target.value);
}}
type="date"
className={`m-0 rounded-sm border-[1px] border-border p-3 text-base ${hasError && 'border-red-400'}`}
/>
{errorText && <span className="pl-1 text-xs text-red-400">{errorText}</span>}
Expand Down
Loading

0 comments on commit 50c8cfd

Please sign in to comment.