Skip to content

Commit

Permalink
Merge branch '67-대회-생성-페이지-구현' of https://github.com/boostcampwm2023/…
Browse files Browse the repository at this point in the history
…web12-algo-with-me into 67-대회-생성-페이지-구현
  • Loading branch information
dev2820 committed Nov 23, 2023
2 parents 8e0e628 + 0169fb6 commit d653950
Show file tree
Hide file tree
Showing 12 changed files with 369 additions and 42 deletions.
11 changes: 11 additions & 0 deletions frontend/src/components/Main/Buttons/GoToCreateCompetitionLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Link } from 'react-router-dom';

export default function GoToCreateCompetitionLink() {
// TODO: 로그인 여부에 따른 페이지 이동 설정

return (
<Link to="/contest/create">
<button>대회 생성</button>
</Link>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default function JoinCompetitionButton() {
// TODO: 대회에 참여하는 로직 작성 / 참여하기 활성화 로직 작성
return <button>참여하기</button>;
}
10 changes: 10 additions & 0 deletions frontend/src/components/Main/Buttons/ViewDashboardButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Link } from 'react-router-dom';

export default function ViewDashboardButton(props: { id: number }) {
const dashboardLink = `/contest/dashboard/${props.id}`;
return (
<Link to={dashboardLink}>
<button>대시보드 보기</button>
</Link>
);
}
134 changes: 134 additions & 0 deletions frontend/src/components/Main/CompetitionList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';

import JoinCompetitionButton from '@/components/Main/Buttons/JoinCompetitionButton';
import ViewDashboardButton from '@/components/Main/Buttons/ViewDashboardButton';
import secToTime from '@/utils/secToTime';
const generateMockData = () => {
// API배포가 완료되면 삭제 에정
return [
{
id: 1,
name: '테스트 대회 이름',
detail: '테스트 대회 설명',
maxParticipants: 70,
startsAt: '2023-11-14T08:35:24.358Z',
endsAt: '2023-11-20T12:13:04.005Z',
createdAt: '2023-11-14T08:35:24.358Z',
updatedAt: '2023-11-21T02:28:43.955Z',
},
{
id: 2,
name: 'ICPC 서울',
detail: '이거슨 아이씨피씨입니다',
maxParticipants: 1000,
startsAt: '2023-11-21T07:10:44.456Z',
endsAt: '2023-11-21T10:10:44.456Z',
createdAt: '2023-11-21T07:50:58.686Z',
updatedAt: '2023-11-21T07:50:58.686Z',
},
{
id: 3,
name: '천하제일코딩대회',
detail: '^오^',
maxParticipants: 10,
startsAt: '2023-11-21T07:10:44.456Z',
endsAt: '2023-11-21T10:10:44.456Z',
createdAt: '2023-11-21T07:57:07.563Z',
updatedAt: '2023-11-21T07:57:07.563Z',
},
{
id: 4,
name: 'fe테스트대회',
detail: '가나다라마바사',
maxParticipants: 3,
startsAt: '2023-11-22T01:20:00.000Z',
endsAt: '2023-11-23T01:20:00.000Z',
createdAt: '2023-11-22T10:22:03.723Z',
updatedAt: '2023-11-22T10:22:03.723Z',
},
{
id: 5,
name: '가나다라',
detail: '마바사아자차카타파하',
maxParticipants: 3,
startsAt: '2023-11-23T03:00:00.000Z',
endsAt: '2023-11-23T04:00:00.000Z',
createdAt: '2023-11-22T12:00:46.942Z',
updatedAt: '2023-11-22T12:00:46.942Z',
},
];
};

interface Competition {
id: number;
name: string;
startsAt: string;
endsAt: string;
maxParticipants: number;
}

const getCompetitionDetailURL = (competitionId: number) => `/contest/detail/${competitionId}`;

function formatTimeRemaining(startsAt: string, endsAt: string): string {
const now = new Date();
const startDate = new Date(startsAt);
const endDate = new Date(endsAt);

if (endDate.getTime() < now.getTime()) {
return '종료';
} else if (startDate.getTime() > now.getTime()) {
const timeDiff = startDate.getTime() - now.getTime();
const { days, hours, minutes, seconds } = secToTime(timeDiff);

return `시작까지 ${days}${hours}:${minutes}:${seconds}`;
} else {
return '진행중';
}
}

export default function CompetitionList() {
const [competitions, setCompetitions] = useState<Competition[]>([]);

useEffect(() => {
// 실제 API 요청 대신 목업 데이터 사용 -> TODO: API배포가 완료되면 API처리하는 코드로 바꿔야함
const mockData = generateMockData();
setCompetitions(mockData);
}, []);

// TODO: 대회 시작 전에 들어와서 대회가 시작된 뒤에 참여 버튼을 누르면 서버에서 거절하고 화면에 alert을 띄우고 새로고침
return (
<div>
<table>
<thead>
<tr>
<th>대회 이름</th>
<th>시작 시간</th>
<th>종료 시간</th>
<th>상태</th>
<th>참여</th>
<th>대시보드</th>
</tr>
</thead>
<tbody>
{competitions.map((competition) => (
<tr key={competition.id}>
<td>
<Link to={getCompetitionDetailURL(competition.id)}>{competition.name}</Link>
</td>
<td>{new Date(competition.startsAt).toLocaleString()}</td>
<td>{new Date(competition.endsAt).toLocaleString()}</td>
<td>{formatTimeRemaining(competition.startsAt, competition.endsAt)}</td>
<td>
{competition.startsAt > new Date().toISOString() && <JoinCompetitionButton />}
</td>
<td>
<ViewDashboardButton id={competition.id} />
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
1 change: 1 addition & 0 deletions frontend/src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const SITE = {
NAME: 'Alog With Me',
PAGE_DESCRIPTION: '천하제일코딩테스트',
};
46 changes: 46 additions & 0 deletions frontend/src/hooks/competition/useCompetition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useEffect, useState } from 'react';

import axios from 'axios';

interface Competition {
id: number;
name: string;
detail: string;
maxParticipants: number;
startsAt: string;
endsAt: string;
createdAt: string;
updatedAt: string;
}

const notFoundCompetition: Competition = {
id: 0,
name: 'Competition Not Found',
detail: 'Competition Not Found',
maxParticipants: 0,
startsAt: 'Competition Not Found',
endsAt: 'Competition Not Found',
createdAt: 'Competition Not Found',
updatedAt: 'Competition Not Found',
};

export const useCompetition = (competitionId: number) => {
const problems = [1, 2, 3]; // TODO: 대회에 해당하는 문제의 id를 유동적으로 채워넣을 수 있게 수정해야함
const [competition, setCompetition] = useState<Competition>(notFoundCompetition);

useEffect(() => {
axios
.get(`http://101.101.208.240:3000/competitions/${competitionId}`)
.then((response) => {
setCompetition(response.data);
})
.catch((error) => {
console.error('Error fetching competition data:', error);
});
}, [competitionId]);

return {
problems,
competition,
};
};
47 changes: 47 additions & 0 deletions frontend/src/hooks/problem/useProblem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useEffect, useState } from 'react';

import axios from 'axios';

interface CompetitionProblem {
id: number;
title: string;
timeLimit: number;
memoryLimit: number;
content: string;
createdAt: string;
solutionCode: string;
testcases: string;
}

const notFoundProblem: CompetitionProblem = {
id: 0,
title: 'Problem Not Found',
timeLimit: 0,
memoryLimit: 0,
content: 'The requested problem could not be found.',
solutionCode: '',
testcases: '',
createdAt: new Date().toISOString(),
};

export const useProblem = (problemId: number) => {
const [problem, setProblem] = useState<CompetitionProblem>(notFoundProblem);

const fetchProblem = async (id: number) => {
try {
const response = await axios.get(`http://101.101.208.240:3000/competitions/problems/${id}`);
const fetchedProblem = response.data;
setProblem(fetchedProblem || notFoundProblem);
} catch (error) {
console.error('Error fetching problem data:', error);
}
};

useEffect(() => {
fetchProblem(problemId);
}, [problemId]);

return {
problem,
};
};
50 changes: 27 additions & 23 deletions frontend/src/pages/ContestPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { css } from '@style/css';

import { useState } from 'react';
import { useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';

import ContestBreadCrumb from '@/components/Contest/ContestBreadCrumb';
import Editor from '@/components/Editor/Editor';
Expand All @@ -9,25 +10,17 @@ import { SimulationInputList } from '@/components/Simulation/SimulationInputList
import { SimulationResultList } from '@/components/Simulation/SimulationResultList';
import SubmissionResult from '@/components/SubmissionResult';
import { SITE } from '@/constants';
import { useCompetition } from '@/hooks/competition/useCompetition';
import { useProblem } from '@/hooks/problem/useProblem';
import { useSimulations } from '@/hooks/simulation';
import mockData from '@/mockData.json';

const notFoundProblem = {
title: 'Problem Not Found',
timeLimit: 0,
memoryLimit: 0,
content: 'The requested problem could not be found.',
solutionCode: '',
simulations: [],
createdAt: new Date().toISOString(),
};

const INITIAL_PROBLEM_ID = 6;

const RUN_SIMULATION = '테스트 실행';
const CANCEL_SIMULATION = '실행 취소';

export default function ContestPage() {
const CONTEST_NAME = 'Test'; // api로 받을 정보
const { id } = useParams<{ id: string }>();
const [currentProblemIndex, setCurrentProblemIndex] = useState(0);

const {
simulationInputs,
simulationResults,
Expand All @@ -36,10 +29,18 @@ export default function ContestPage() {
changeInput,
cancelSimulation,
} = useSimulations();
const [currentProblemId] = useState(INITIAL_PROBLEM_ID);
const targetProblem =
mockData.problems.find((problem) => problem.id === currentProblemId) || notFoundProblem;
const [code, setCode] = useState<string>(targetProblem.solutionCode);
const competitionId: number = id ? parseInt(id, 10) : -1;

const { problems, competition } = useCompetition(competitionId);
const currentProblemId = useMemo(() => {
return problems[currentProblemIndex];
}, [problems, currentProblemIndex]);

const { problem } = useProblem(currentProblemId);

const [code, setCode] = useState<string>(problem.solutionCode);

const crumbs = [SITE.NAME, competition.name, problem.title];

const handleChangeCode = (newCode: string) => {
setCode(newCode);
Expand All @@ -57,18 +58,21 @@ export default function ContestPage() {
changeInput(id, newParam);
};

const crumbs = [SITE.NAME, CONTEST_NAME, targetProblem.title];
const handleNextProblem = () => {
setCurrentProblemIndex(currentProblemIndex + 1);
};

return (
<main className={style}>
<button onClick={handleNextProblem}>다음 문제</button>
<ContestBreadCrumb crumbs={crumbs} />
<section>
<span className={problemTitleStyle}>{targetProblem.title}</span>
<span className={problemTitleStyle}>{problem.title}</span>
</section>
<section className={rowListStyle}>
<ProblemViewer content={targetProblem.content}></ProblemViewer>
<ProblemViewer content={problem.content}></ProblemViewer>
<div className={colListStyle}>
<Editor code={code} onChangeCode={handleChangeCode}></Editor>
<Editor code={problem.solutionCode} onChangeCode={handleChangeCode}></Editor>
<SimulationInputList
inputList={simulationInputs}
onChangeInput={handleChangeInput}
Expand Down
28 changes: 28 additions & 0 deletions frontend/src/pages/MainPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { css } from '@style/css';

import GoToCreateCompetitionLink from '@/components/Main/Buttons/GoToCreateCompetitionLink';
import CompetitionList from '@/components/Main/CompetitionList';
import { SITE } from '@/constants';

function MainPage() {
return (
<main className={style}>
<span className={ProjectNameStyle}>{SITE.NAME} </span>
<span>{SITE.PAGE_DESCRIPTION} </span>
<GoToCreateCompetitionLink />
<CompetitionList />
</main>
);
}

export default MainPage;

const ProjectNameStyle = css({
fontSize: '70px',
});

const style = css({
display: 'grid',
placeItems: 'center',
height: '500px',
});
Loading

0 comments on commit d653950

Please sign in to comment.