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/#234 대회 상세 페이지 구현 #235

Merged
merged 14 commits into from
Dec 17, 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
5 changes: 5 additions & 0 deletions frontend/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ const nextConfig = {
hostname: 'localhost',
pathname: '**',
},
{
protocol: 'https',
hostname: 'www.pusan.ac.kr',
pathname: '**',
},
{
protocol: 'https',
hostname: 'swcss.pusan.ac.kr',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,9 @@ const HackathonTeamCreateModal = ({ hackathonId, open, onClose }: HackathonTeamC
registerTeam(
{
hackathonId: hackathonId,
image: values.image,
name: values.name,
work: values.work,
thumbnailImage: values.image,
teamName: values.name,
projectTitle: values.work,
githubUrl: values.githubUrl,
members: [values.leader, ...values.members],
password: values.password,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const HackathonTeamReadModal = ({ selectedTeam, onClose }: HackathonTeamReadModa
className="flex h-[600px] w-full max-w-[900px] flex-col items-center gap-2 rounded bg-white p-5 sm:h-[700px]"
>
<div className="flex w-full flex-wrap items-center justify-between gap-y-2">
<PageTitle title={selectedTeam.name} />
<PageTitle title={selectedTeam.teamName} description={selectedTeam.projectTitle} />
<div className="flex flex-grow justify-end gap-2">
<span className="flex items-center gap-1 font-bold text-primary-main">
<VscHeart />
Expand All @@ -67,7 +67,7 @@ const HackathonTeamReadModal = ({ selectedTeam, onClose }: HackathonTeamReadModa
<tr key={member.id} className={classNames(member.isLeader && 'font-bold')}>
<td className="min-w-[4em]">{member.name}</td>
<td className="min-w-[10em]">{member.id}</td>
<td className="hidden min-w-[10em] md:table-cell">{member.majorName}</td>
<td className="hidden min-w-[10em] md:table-cell">{member.major}</td>
{member.isLeader && <td className="min-w-[4em]">(팀장)</td>}
</tr>
))}
Expand Down
9 changes: 6 additions & 3 deletions frontend/src/app/(client)/hackathon/[slug]/vote/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Image from 'next/image';
import { useState } from 'react';
import HackathonTeamCreateModal from './components/HackathonTeamCreateModal';
import HackathonTeamReadModal from './components/HackathonTeamReadModal';
import { mockHackathonTeamPageableData } from '@/mocks/hackathon';

interface HackathonVotePageProps {
params: {
Expand All @@ -20,7 +21,9 @@ const Page = ({ params: { slug }, searchParams }: HackathonVotePageProps) => {
const [teamCreateModalOpen, setTeamCreateModalOpen] = useState<boolean>(false);

const page = searchParams?.page ? parseInt(searchParams.page, 10) : 1;
const { data: teams } = useHackathonTeamsQuery(slug, page, 8);
// const { data: teams } = useHackathonTeamsQuery(slug, page, 8);
const teams = mockHackathonTeamPageableData;

return (
<div className="flex flex-col gap-4">
<HackathonTeamReadModal selectedTeam={selectedTeam} onClose={() => setSelectedTeam(null)} />
Expand Down Expand Up @@ -48,7 +51,7 @@ const Page = ({ params: { slug }, searchParams }: HackathonVotePageProps) => {
>
<div className="relative h-28 w-full">
<Image
src={process.env.NEXT_PUBLIC_FILE_URL + '/' + team.thumbnailImageName}
src={process.env.NEXT_PUBLIC_FILE_URL + '/' + team.thumbnailImage}
alt="팀 섬네일"
className="rounded-t-sm"
layout="fill"
Expand All @@ -57,7 +60,7 @@ const Page = ({ params: { slug }, searchParams }: HackathonVotePageProps) => {
quality={100}
/>
</div>
<p className="m-2 font-bold">{team.name}</p>
<p className="m-2 font-bold">{team.teamName}</p>
<div className="flex justify-end">
<span className="ml-4 flex-grow rounded-l-2xl border border-r-0 border-primary-main p-2 text-left text-lg text-primary-main">
{team.voteCount}표 득표
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/app/(client)/hackathon/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ const Page = async ({ searchParams }: { searchParams?: { [key: string]: string |
/>
<div className="h-0 w-full border border-border" />

<div className="flex h-40 w-full items-center justify-center text-comment">개발 중인 기능입니다.</div>
{/* <div className="flex h-40 w-full items-center justify-center text-comment">개발 중인 기능입니다.</div> */}

{/* {hackathons?.content && hackathons.content.length > 0 ? (
{hackathons?.content && hackathons.content.length > 0 ? (
<div className="grid grid-cols-1 gap-6 py-4 sm:grid-cols-2 md:grid-cols-3">
{hackathons.content.map((hackathon) => (
<Link
Expand Down Expand Up @@ -104,7 +104,7 @@ const Page = async ({ searchParams }: { searchParams?: { [key: string]: string |
<div className="flex h-40 w-full items-center justify-center text-comment">해커톤 정보가 없습니다.</div>
)}

<Pagination currentPage={page} totalItems={hackathons?.totalElements ?? 0} pathname={pathname} pageSize={6} /> */}
<Pagination currentPage={page} totalItems={hackathons?.totalElements ?? 0} pathname={pathname} pageSize={6} />
</div>
);
};
Expand Down
197 changes: 197 additions & 0 deletions frontend/src/app/admin/hackathon/edit/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
'use client';

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

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

export interface AdminHackathonEditPageProps {
params: { slug: number };
}

export type HackathonInfo = Omit<HackathonDto, 'bannerImage' | 'id'> & { bannerImage: File | null };

export default function AdminHackathonEditPage({ params: { slug } }: AdminHackathonEditPageProps) {
// TODO: API 연결
const hackathonInfoInitialValue: HackathonInfo = {
title: '',
content: '',
bannerImage: null,
applyStartDate: '',
applyEndDate: '',
hackathonStartDate: '',
hackathonEndDate: '',
teamCode: '',
};

return (
<div className="w-full">
<PageTitle title="해커톤 수정" />
<Formik
initialValues={hackathonInfoInitialValue}
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.title}
onChange={handleChange}
onBlur={handleBlur}
errorText={touched.title && errors.title ? errors.title : 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>
);
}

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('대회에 참여할 수 있는 팀 등록 코드를 입력해주세요.'),
});
33 changes: 33 additions & 0 deletions frontend/src/app/admin/hackathon/manage/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import PageTitle from '@/components/common/PageTitle';
import AdminHackathonManageTeamSearchBox from '@/components/ui/admin/hackathon/AdminHackathonManageTeamSearchBox';
import AdminHackathonManageTeamTable from '@/components/ui/admin/hackathon/AdminHackathonManageTeamTable';
import { getAuthFromCookie } from '@/lib/utils/auth';
import { mockHackathonTeamPageableData } from '@/mocks/hackathon';
import { AuthSliceState } from '@/store/auth.slice';
import { headers } from 'next/headers';

export interface AdminHackathonEditPageProps {
params: { slug: number };
searchParams?: { [key: string]: string | undefined };
}

export default function AdminHackathonManagePage({ params: { slug }, searchParams }: AdminHackathonEditPageProps) {
const headersList = headers();
const pathname = headersList.get('x-pathname') || '';

const auth: AuthSliceState = getAuthFromCookie();

const page = searchParams?.page ? parseInt(searchParams.page, 10) : 1;
const keyword = searchParams?.keyword ? searchParams.keyword : '';

// TODO: api 연결
const hackathonTeamInfos = mockHackathonTeamPageableData.content;

return (
<>
<PageTitle title="해커톤 관리" className="mb-4" />
<AdminHackathonManageTeamSearchBox hackathonId={slug} count={hackathonTeamInfos.length} keyword={keyword} />
<AdminHackathonManageTeamTable hackathonId={slug} teamInfos={hackathonTeamInfos} />
</>
);
}
4 changes: 2 additions & 2 deletions frontend/src/app/admin/hackathon/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import { getAuthFromCookie } from '@/lib/utils/auth';
import { AuthSliceState } from '@/store/auth.slice';
import Pagination from '@/components/common/Pagination';

export interface HackathonListPageProps {
export interface AdminHackathonListPageProps {
searchParams?: { [key: string]: string | undefined };
}

export default function HackathonListPage({ searchParams }: HackathonListPageProps) {
export default function AdminHackathonListPage({ searchParams }: AdminHackathonListPageProps) {
const headersList = headers();
const pathname = headersList.get('x-pathname') || '';

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/admin/hackathon/register/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { MdTextFields } from '@react-icons/all-files/md/MdTextFields';
import { MdDateRange } from '@react-icons/all-files/md/MdDateRange';
import { HackathonDto } from '@/types/common.dto';

export default function HackathonCreatePage() {
export default function AdminHackathonCreatePage() {
return (
<div className="w-full">
<PageTitle title="해커톤 등록" />
Expand Down
Loading