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

Feature/#230 대회 등록 페이지 구현 #231

Merged
merged 11 commits into from
Dec 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

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