-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'fe-dev' into 132-대회-페이지-내의-웹소켓-연결-상태를-확인할-수-있는-기능
- Loading branch information
Showing
25 changed files
with
597 additions
and
305 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import api from '@/utils/api'; | ||
|
||
import { Competition } from './types'; | ||
|
||
export const fetchCompetitionList = async (): Promise<Competition[]> => { | ||
try { | ||
const response = await api.get('/competitions'); | ||
return response.data; | ||
} catch (error) { | ||
console.error('Error fetching competitions:', (error as Error).message); | ||
throw error; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export type Competition = { | ||
id: number; | ||
name: string; | ||
startsAt: string; | ||
endsAt: string; | ||
maxParticipants: number; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import api from '@/utils/api'; | ||
|
||
import type { CompetitionApiData } from './types'; | ||
import axios from 'axios'; | ||
|
||
const STATUS = { | ||
Forbidden: 403, | ||
BadRequest: 400, | ||
} as const; | ||
|
||
export async function joinCompetition(data: CompetitionApiData) { | ||
const { id, token } = data; | ||
|
||
try { | ||
await api.post( | ||
`/competitions/${id}/participations`, | ||
{}, | ||
{ | ||
headers: { | ||
Authorization: `Bearer ${token}`, | ||
}, | ||
}, | ||
); | ||
|
||
return '대회에 성공적으로 참여했습니다.'; | ||
} catch (error: unknown) { | ||
if (!axios.isAxiosError(error)) { | ||
return 'Unexpected error occurred'; | ||
} | ||
|
||
if (!error.response) { | ||
return 'Network error occurred'; | ||
} | ||
|
||
switch (error.response.status) { | ||
case STATUS.Forbidden: | ||
return '대회 참여에 실패했습니다. 서버에서 거절되었습니다.'; | ||
case STATUS.BadRequest: | ||
return '이미 참여한 대회입니다.'; | ||
default: | ||
return `HTTP Error ${error.response.status}`; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export type CompetitionApiData = { | ||
id: number; | ||
token: string | null; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import { css, cx } from '@style/css'; | ||
|
||
import type { HTMLAttributes, MouseEvent } from 'react'; | ||
import { useContext, useEffect, useRef } from 'react'; | ||
import ReactDOM from 'react-dom'; | ||
|
||
import { ModalContext } from './ModalContext'; | ||
import { ModalProvider } from './ModalProvider'; | ||
|
||
export interface Props extends HTMLAttributes<HTMLDialogElement> { | ||
onBackdropPressed?: () => void; | ||
} | ||
|
||
export function Modal({ onBackdropPressed, children, ...props }: Props) { | ||
const modal = useContext(ModalContext); | ||
const $dialog = useRef<HTMLDialogElement>(null); | ||
|
||
const handleClickBackdrop = (e: MouseEvent<HTMLDialogElement>) => { | ||
const $target = e.target as HTMLDialogElement; | ||
|
||
if ($target.nodeName !== 'DIALOG') return; | ||
|
||
if (onBackdropPressed instanceof Function) { | ||
onBackdropPressed(); | ||
} | ||
}; | ||
|
||
useEffect(() => { | ||
if (modal.isOpen) { | ||
$dialog.current?.showModal(); | ||
} else { | ||
$dialog.current?.close(); | ||
} | ||
}, [modal.isOpen]); | ||
|
||
return ReactDOM.createPortal( | ||
<dialog | ||
ref={$dialog} | ||
className={cx(style, dialogStyle)} | ||
aria-modal="true" | ||
aria-labelledby="dialog-title" | ||
onClick={handleClickBackdrop} | ||
{...props} | ||
> | ||
<div className={contentStyle}>{children}</div> | ||
</dialog>, | ||
document.body, | ||
); | ||
} | ||
|
||
Modal.Context = ModalContext; | ||
Modal.Provider = ModalProvider; | ||
|
||
const style = css({ | ||
borderRadius: '0.5rem', | ||
}); | ||
|
||
const dialogStyle = css({ | ||
position: 'fixed', | ||
left: '50%', | ||
top: '50%', | ||
transform: 'translate(-50%,-50%)', | ||
width: '500px', | ||
height: '400px', | ||
_backdrop: { | ||
background: 'rgba(00,00,00,0.5)', | ||
backdropFilter: 'blur(1rem)', | ||
}, | ||
}); | ||
|
||
const contentStyle = css({ | ||
width: '100%', | ||
height: '100%', | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { createContext } from 'react'; | ||
|
||
interface ModalContextProps { | ||
isOpen: boolean; | ||
close: () => void; | ||
open: () => void; | ||
} | ||
|
||
export const ModalContext = createContext<ModalContextProps>({ | ||
isOpen: false, | ||
close: () => {}, | ||
open: () => {}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import type { ReactNode } from 'react'; | ||
import { useState } from 'react'; | ||
|
||
import { ModalContext } from './ModalContext'; | ||
|
||
export function ModalProvider({ children }: { children: ReactNode }) { | ||
const [isOpen, setIsOpen] = useState<boolean>(false); | ||
const close = () => { | ||
setIsOpen(false); | ||
}; | ||
const open = () => { | ||
setIsOpen(true); | ||
}; | ||
|
||
return ( | ||
<ModalContext.Provider | ||
value={{ | ||
isOpen, | ||
close, | ||
open, | ||
}} | ||
> | ||
{children} | ||
</ModalContext.Provider> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { Modal } from './Modal'; | ||
export type { Props as ModalProps } from './Modal'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export { Input } from './Input'; | ||
export * from './Modal'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { css } from '@style/css'; | ||
|
||
import ViewDashboardButton from '../Main/Buttons/ViewDashboardButton'; | ||
import ContestBreadCrumb from './ContestBreadCrumb'; | ||
|
||
interface Props { | ||
crumbs: string[]; | ||
id: number; | ||
} | ||
|
||
export default function CompetitionHeader(props: Props) { | ||
return ( | ||
<div className={headerStyle}> | ||
<ContestBreadCrumb crumbs={props.crumbs} /> | ||
<ViewDashboardButton id={props.id} /> | ||
</div> | ||
); | ||
} | ||
|
||
const headerStyle = css({ | ||
backgroundColor: 'gray', | ||
color: 'black', | ||
width: '850px', | ||
height: '50px', | ||
display: 'flex', | ||
justifyContent: 'space-between', | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
frontend/src/components/Contest/ContestProblemSelector.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { css } from '@style/css'; | ||
|
||
interface AsideProps { | ||
problemIds: number[]; | ||
onChangeProblemIndex: (index: number) => void; | ||
} | ||
|
||
export default function ContestProblemSelector(props: AsideProps) { | ||
function handleChangeProblemIndex(index: number) { | ||
props.onChangeProblemIndex(index); | ||
} | ||
|
||
return ( | ||
<aside> | ||
<span>문제 목록</span> | ||
<ul> | ||
{props.problemIds.map((id: number, index: number) => ( | ||
<li key={id}> | ||
<button className={selectProblemStyle} onClick={() => handleChangeProblemIndex(index)}> | ||
문제 {index + 1} | ||
</button> | ||
</li> | ||
))} | ||
</ul> | ||
</aside> | ||
); | ||
} | ||
|
||
const selectProblemStyle = css({ | ||
color: 'black', | ||
}); |
22 changes: 15 additions & 7 deletions
22
frontend/src/components/Main/Buttons/GoToCreateCompetitionLink.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,19 @@ | ||
import { Link } from 'react-router-dom'; | ||
import { useNavigate } from 'react-router-dom'; | ||
|
||
import useAuth from '@/hooks/login/useAuth'; | ||
|
||
export default function GoToCreateCompetitionLink() { | ||
// TODO: 로그인 여부에 따른 페이지 이동 설정 | ||
const { isLoggedin } = useAuth(); | ||
const navigate = useNavigate(); | ||
|
||
const handleNavigate = () => { | ||
if (!isLoggedin) { | ||
alert('로그인이 필요합니다.'); | ||
navigate('/login'); | ||
} else { | ||
navigate('/contest/create'); | ||
} | ||
}; | ||
|
||
return ( | ||
<Link to="/contest/create"> | ||
<button>대회 생성</button> | ||
</Link> | ||
); | ||
return <button onClick={handleNavigate}>대회 생성</button>; | ||
} |
35 changes: 32 additions & 3 deletions
35
frontend/src/components/Main/Buttons/JoinCompetitionButton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,33 @@ | ||
export default function JoinCompetitionButton() { | ||
// TODO: 대회에 참여하는 로직 작성 / 참여하기 활성화 로직 작성 | ||
return <button>참여하기</button>; | ||
import { useNavigate } from 'react-router-dom'; | ||
|
||
import { joinCompetition } from '@/apis/joinCompetition'; | ||
import type { CompetitionApiData } from '@/apis/joinCompetition/types'; | ||
import useAuth from '@/hooks/login/useAuth'; | ||
|
||
const TOKEN_KEY = 'accessToken'; | ||
|
||
export default function JoinCompetitionButton(props: { id: number }) { | ||
const { isLoggedin } = useAuth(); | ||
const navigate = useNavigate(); | ||
|
||
const queryParams = new URLSearchParams(location.search); | ||
const token = queryParams.get(TOKEN_KEY) || localStorage.getItem(TOKEN_KEY); | ||
|
||
const handleJoinClick = async () => { | ||
if (!isLoggedin) { | ||
alert('로그인이 필요합니다.'); | ||
navigate('/login'); | ||
return; | ||
} | ||
|
||
const result = await joinCompetition(competitionData); | ||
alert(result); | ||
window.location.reload(); | ||
}; | ||
const competitionData: CompetitionApiData = { | ||
id: props.id, | ||
token: token, | ||
}; | ||
|
||
return <button onClick={handleJoinClick}>참여하기</button>; | ||
} |
Oops, something went wrong.