Skip to content

Commit

Permalink
Merge pull request #149 from boostcampwm2023/138-대회-세부정보-페이지-생성
Browse files Browse the repository at this point in the history
[#138] 대회 세부정보 페이지 생성
  • Loading branch information
dmdmdkdkr authored Nov 29, 2023
2 parents 51ba82e + 80981df commit 71602ed
Show file tree
Hide file tree
Showing 10 changed files with 332 additions and 0 deletions.
38 changes: 38 additions & 0 deletions frontend/src/components/CompetitionDetail/AfterCompetition.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { css } from '@style/css';

import { CompetitionInfo } from '@/apis/competitions';

import CompetitionDetailInfo from './CompetitionDetailInfo';
import ProblemList from './ProblemList';

interface Props {
competitionId: number;
competition: CompetitionInfo;
competitionSchedule: string;
}

const AFTER_COMPETITION_TEXT = ' 종료';

export default function AfterCompetition({
competitionId,
competition,
competitionSchedule,
}: Props) {
return (
<div className={containerStyle}>
<CompetitionDetailInfo
competition={competition}
text={AFTER_COMPETITION_TEXT}
competitionSchedule={competitionSchedule}
/>
<ProblemList competitionId={competitionId} />
</div>
);
}

const containerStyle = css({
justifyContent: 'space-between',
alignItems: 'center',
padding: '16px',
border: '1px solid #ccc',
});
51 changes: 51 additions & 0 deletions frontend/src/components/CompetitionDetail/BeforeCompetition.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { css } from '@style/css';

import { CompetitionInfo } from '@/apis/competitions';

import JoinCompetitionButton from '../Main/Buttons/JoinCompetitionButton';
import EnterCompetitionButton from './Buttons/EnterCompetitionButton';
import CompetitionDetailInfo from './CompetitionDetailInfo';

interface Props {
competitionId: number;
competition: CompetitionInfo;
startsAt: Date;
endsAt: Date;
competitionSchedule: string;
}

export default function BeforeCompetition({
competitionId,
competition,
startsAt,
endsAt,
competitionSchedule,
}: Props) {
const BEFORE_COMPETITION_TEXT = ` 시작 전`;

return (
<div className={containerStyle}>
<CompetitionDetailInfo
competition={competition}
text={BEFORE_COMPETITION_TEXT}
competitionSchedule={competitionSchedule}
/>
<div className={buttonContainerStyle}>
<JoinCompetitionButton id={competitionId} />
<EnterCompetitionButton id={competitionId} startsAt={startsAt} endsAt={endsAt} />
</div>
</div>
);
}

const containerStyle = css({
justifyContent: 'space-between',
alignItems: 'center',
padding: '16px',
border: '1px solid #ccc',
});

const buttonContainerStyle = css({
display: 'flex',
gap: '16px',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useNavigate } from 'react-router-dom';

import useAuth from '@/hooks/login/useAuth';

interface Props {
id: number;
startsAt: Date;
endsAt: Date;
}

export default function EnterCompetitionButton({ id, startsAt, endsAt }: Props) {
const competitionLink = `/contest/${id}`;
const { isLoggedin } = useAuth();
const navigate = useNavigate();

const handleNavigate = () => {
const currentTime = new Date();

if (!isLoggedin) {
alert('로그인이 필요합니다.');
navigate('/login');
} else if (currentTime < startsAt) {
alert('아직 대회가 시작되지 않았습니다. 다시 시도해주세요');
window.location.reload();
} else if (currentTime >= endsAt) {
alert('해당 대회는 종료되었습니다.');
window.location.reload();
} else {
navigate(competitionLink);
}
};

return <button onClick={handleNavigate}>대회 입장</button>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// CompetitionDetailContent.js
import { CompetitionInfo } from '@/apis/competitions';
import AfterCompetition from '@/components/CompetitionDetail/AfterCompetition';
import BeforeCompetition from '@/components/CompetitionDetail/BeforeCompetition';
import DuringCompetition from '@/components/CompetitionDetail/DuringCompetition';

interface Props {
competitionId: number;
competition: CompetitionInfo;
startsAt: Date;
endsAt: Date;
competitionSchedule: string;
}

export function CompetitionDetailContent({
competitionId,
competition,
startsAt,
endsAt,
competitionSchedule,
}: Props) {
const currentDate = new Date();

if (currentDate < startsAt) {
return (
<BeforeCompetition
{...{ competitionId, competition, startsAt, endsAt, competitionSchedule }}
/>
);
} else if (currentDate < endsAt) {
return (
<DuringCompetition
{...{ competitionId, competition, startsAt, endsAt, competitionSchedule }}
/>
);
} else {
return <AfterCompetition {...{ competitionId, competition, competitionSchedule }} />;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { css } from '@style/css';

import { CompetitionInfo } from '@/apis/competitions';

interface Props {
competition: CompetitionInfo;
text: string;
competitionSchedule: string;
}

export default function CompetitionDetailInfo({ competition, text, competitionSchedule }: Props) {
return (
<div className={infoContainerStyle}>
<div>
<span className={competitionNameStyle}>{competition.name}</span>
<span className={statusTextStyle}>{text}</span>
</div>
<span className={statusTextStyle}>{competitionSchedule}</span>
<div className={additionalTextStyle}>{competition.detail}</div>
</div>
);
}

const infoContainerStyle = css({
display: 'flex',
flexDirection: 'column',
});

const competitionNameStyle = css({
fontSize: '18px',
fontWeight: 'bold',
color: 'black',
marginBottom: '8px',
});

const statusTextStyle = css({
fontSize: '12px',
color: 'gray',
marginBottom: '8px',
});

const additionalTextStyle = css({
fontSize: '14px',
color: 'black',
marginBottom: '8px',
});
42 changes: 42 additions & 0 deletions frontend/src/components/CompetitionDetail/DuringCompetition.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { css } from '@style/css';

import { CompetitionInfo } from '@/apis/competitions';

import EnterCompetitionButton from './Buttons/EnterCompetitionButton';
import CompetitionDetailInfo from './CompetitionDetailInfo';

interface Props {
competitionId: number;
competition: CompetitionInfo;
startsAt: Date;
endsAt: Date;
competitionSchedule: string;
}

const DURING_COMPETITION_TEXT = ' 진행중';

export default function DuringCompetition({
competitionId,
competition,
startsAt,
endsAt,
competitionSchedule,
}: Props) {
return (
<div className={containerStyle}>
<CompetitionDetailInfo
competition={competition}
text={DURING_COMPETITION_TEXT}
competitionSchedule={competitionSchedule}
/>
<EnterCompetitionButton id={competitionId} startsAt={startsAt} endsAt={endsAt} />
</div>
);
}

const containerStyle = css({
justifyContent: 'space-between',
alignItems: 'center',
padding: '16px',
border: '1px solid #ccc',
});
34 changes: 34 additions & 0 deletions frontend/src/components/CompetitionDetail/ProblemList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Link } from 'react-router-dom';

import { useCompetitionProblemList } from '@/hooks/problem';

interface Props {
competitionId: number;
}

export default function ProblemList({ competitionId }: Props) {
const { problemList } = useCompetitionProblemList(competitionId);

return (
<div>
<table>
<thead>
<tr>
<th>번호</th>
<th>문제 제목</th>
</tr>
</thead>
<tbody>
{problemList.map((problem) => (
<tr key={problem.id}>
<td>{problem.id}</td>
<td>
<Link to={`/problem/${problem.id}`}>{problem.title}</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
32 changes: 32 additions & 0 deletions frontend/src/pages/CompetitionDetailPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useParams } from 'react-router-dom';

import { CompetitionDetailContent } from '@/components/CompetitionDetail/CompetitionDetailContent';
import Header from '@/components/Header';
import { useCompetition } from '@/hooks/competition';
import { formatDate } from '@/utils/date';

export default function CompetitionDetailPage() {
const { id } = useParams<{ id: string }>();
const competitionId: number = id ? parseInt(id, 10) : -1;
const { competition } = useCompetition(competitionId);
// 대회 상태에 따른 페이지를 구성하기 위해 현재 날짜, 시작 시간, 종료 시간을 가져옴
const startsAt = new Date(competition?.startsAt || '');
const endsAt = new Date(competition?.endsAt || '');
const formattedStartsAt = formatDate(startsAt, 'YYYY. MM. DD. hh:mm');
const formattedEndsAt = formatDate(endsAt, 'YYYY. MM. DD. hh:mm');

const competitionSchedule = `시작: ${formattedStartsAt} 종료: ${formattedEndsAt}`;

return (
<div>
<Header />
<CompetitionDetailContent
competitionId={competitionId}
competition={competition}
startsAt={startsAt}
endsAt={endsAt}
competitionSchedule={competitionSchedule}
/>
</div>
);
}
5 changes: 5 additions & 0 deletions frontend/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import MainPage from '@/pages/MainPage';
import ProblemPage from '@/pages/ProblemPage';

import App from './App';
import CompetitionDetailPage from './pages/CompetitionDetailPage';

const router = createBrowserRouter([
{
Expand All @@ -30,6 +31,10 @@ const router = createBrowserRouter([
element: <CreateCompetitionPage />,
},
{ path: '/login', element: <LoginPage /> },
{
path: '/contest/detail/:id',
element: <CompetitionDetailPage />,
},
],
},
]);
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/utils/date/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ export const formatDate = (date: Date, form: string) => {
return date.toISOString().slice(0, 'YYYY-MM-DDThh:mm'.length);
}

if (form === 'YYYY. MM. DD. hh:mm') {
return date.toLocaleString('ko-KR', {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
hour12: false,
});
}

return '';
};

Expand Down

0 comments on commit 71602ed

Please sign in to comment.