-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[FE] - 리더보드 UI 구현 및 useQuery + emitAckEvent Wrapper 함수 구현 (#149)
* chore: tanstack/react-query-devtools install * fix: session end 라우팅 주소 변경 * feat: Ack 응답의 emit 이벤트 Promise로 감싸는 Wrapping 함수 - useQuery로 사용하기 위해서 구현 * feat: react-query 기본값 설정 및 devtools 설정 * feat: leaderboard custom hook 구현 - 받아온 데이터 캐시에서 관리 * feat: leaderboard UI 구현
- Loading branch information
1 parent
5a5676a
commit c4f30a9
Showing
16 changed files
with
304 additions
and
6 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Binary file added
BIN
+2.36 MB
.yarn/cache/@tanstack-query-devtools-npm-5.61.4-757435dc28-44886dbf92.zip
Binary file not shown.
Binary file added
BIN
+108 KB
.yarn/cache/@tanstack-react-query-devtools-npm-5.62.0-c120ddf122-b5f94368d8.zip
Binary file not shown.
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
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
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
21 changes: 21 additions & 0 deletions
21
packages/client/src/pages/leaderboard/ConfettiBackground.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,21 @@ | ||
export default function ConfettiBackground() { | ||
return ( | ||
<div className="absolute w-dvw "> | ||
{Array.from({ length: 20 }).map((_, index) => ( | ||
<div | ||
key={index} | ||
className="absolute animate-[confetti_linear_infinite] w-2 h-2" | ||
style={{ | ||
left: `${Math.random() * 100}%`, | ||
top: `-20px`, | ||
animationDelay: `${Math.random() * 3}s`, | ||
animationDuration: `${3 + Math.random() * 2}s`, | ||
backgroundColor: ['#FFD700', '#FF3E4D', '#4CAF50', '#2196F3'][ | ||
Math.floor(Math.random() * 4) | ||
], | ||
}} | ||
/> | ||
))} | ||
</div> | ||
); | ||
} |
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,46 @@ | ||
import { Ranking } from '.'; | ||
|
||
interface PlayerListProps { | ||
players: Ranking[]; | ||
} | ||
|
||
export default function PlayerList({ players }: PlayerListProps) { | ||
return ( | ||
<div className="space-y-2 mt-10"> | ||
{players.map((player, index) => ( | ||
<div | ||
key={index + 3} | ||
className="group flex items-center p-3 gap-3 rounded-xl hover:bg-white/80 hover:shadow-sm transition-all duration-200" | ||
> | ||
{/* Rank */} | ||
<div className="w-12 h-12 rounded-base bg-gradient-to-br from-blue-100 to-blue-50 border border-gray-100 flex items-center justify-center font-bold text-blue-600 text-xl group-hover:animate-bounce"> | ||
{index + 4} | ||
</div> | ||
|
||
{/* Status & Name */} | ||
<div className="flex-1 flex items-center gap-4"> | ||
{/* <div | ||
className={`w-2 h-2 rounded-full ${player.isOnline ? 'bg-green-500' : 'bg-gray-300'}`} | ||
/> */} | ||
<span className="font-semibold text-lg text-gray-800">{player.nickname}</span> | ||
</div> | ||
|
||
{/* Stats */} | ||
<div className="flex items-center gap-6 text-sm"> | ||
{/* <div className="flex items-center gap-2 text-blue-600"> */} | ||
{/* <CheckCircle2 className="w-4 h-4" /> | ||
<span> | ||
{player.correct}/{player.total} | ||
</span> | ||
</div> | ||
<div className="flex items-center gap-2 text-purple-600"> | ||
<Timer className="w-4 h-4" /> | ||
<span>{player.speed}s</span> | ||
</div> */} | ||
<div className="w-20 text-lg text-right font-semibold">{player.score}점</div> | ||
</div> | ||
</div> | ||
))} | ||
</div> | ||
); | ||
} |
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,72 @@ | ||
import { Ranking } from '.'; | ||
import DogImage from '@/shared/assets/characters/강아지.png'; | ||
import CatImage from '@/shared/assets/characters/고양이.png'; | ||
import PigImage from '@/shared/assets/characters/돼지.png'; | ||
import RabbitImage from '@/shared/assets/characters/토끼.png'; | ||
import PenguinImage from '@/shared/assets/characters/펭귄.png'; | ||
import HamsterImage from '@/shared/assets/characters/햄스터.png'; | ||
|
||
interface TopPlayerProps { | ||
players: Ranking[]; | ||
} | ||
|
||
const characters = [DogImage, CatImage, PigImage, RabbitImage, PenguinImage, HamsterImage]; | ||
const characterNames = ['강아지', '고양이', '돼지', '토끼', '펭귄', '햄스터']; | ||
|
||
const Nickname = ({ nickname }: { nickname: string }) => { | ||
return ( | ||
<div className="text-xl font-semibold rounded-base bg-white p-2 shadow-md text-gray-600"> | ||
{nickname} | ||
</div> | ||
); | ||
}; | ||
export default function TopPlayer({ players }: TopPlayerProps) { | ||
return ( | ||
<div className="flex justify-center items-end gap-20 p-4"> | ||
<div className="flex flex-col items-center gap-2"> | ||
<div className="rounded-full w-24 h-24 "> | ||
<img | ||
src={characters[players[1]?.character] ?? DogImage} | ||
alt={`${characterNames[players[1]?.character]}character`} | ||
/> | ||
</div> | ||
<div className="relative w-24 h-28 bg-gradient-to-t from-gray-300 to-gray-200 rounded-base"> | ||
<span className="absolute top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2 text-2xl font-semibold text-gray-600"> | ||
{players[1]?.score} | ||
</span> | ||
</div> | ||
<Nickname nickname={players[1]?.nickname} /> | ||
</div> | ||
|
||
<div className="flex flex-col items-center gap-2"> | ||
<div className="rounded-full w-24 h-24 "> | ||
<img | ||
src={characters[players[0]?.character] ?? DogImage} | ||
alt={`${characterNames[players[0]?.character]}character`} | ||
/> | ||
</div> | ||
<div className="relative w-28 h-40 bg-gradient-to-t from-yellow-300 to-yellow-50 rounded-base"> | ||
<span className="absolute top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2 text-2xl font-semibold text-gray-600"> | ||
{players[0]?.score} | ||
</span> | ||
</div> | ||
<Nickname nickname={players[0]?.nickname} /> | ||
</div> | ||
|
||
<div className="flex flex-col items-center gap-2"> | ||
<div className="rounded-full w-24 h-24 "> | ||
<img | ||
src={characters[players[2]?.character] ?? DogImage} | ||
alt={`${characterNames[players[2]?.character]}character`} | ||
/> | ||
</div> | ||
<div className="relative w-24 h-20 bg-gradient-to-t from-orange-300 to-orange-200 rounded-base"> | ||
<span className="absolute top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2 text-2xl font-semibold text-gray-600"> | ||
{players[2]?.score} | ||
</span> | ||
</div> | ||
<Nickname nickname={players[2]?.nickname} /> | ||
</div> | ||
</div> | ||
); | ||
} |
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,3 +1,58 @@ | ||
import { useParams } from 'react-router-dom'; | ||
|
||
import { getQuizSocket } from '@/shared/utils/socket'; | ||
import TopPlayer from './TopPlayer'; | ||
import PlayerList from './PlayerList'; | ||
import ConfettiBackground from './ConfettiBackground'; | ||
import { useLeaderboard } from './model/hooks/useLeaderboard'; | ||
|
||
export interface Ranking { | ||
nickname: string; | ||
score: number; | ||
character: number; | ||
} | ||
|
||
export default function Leaderboard() { | ||
return <div>Leaderboard</div>; | ||
const { pinCode } = useParams(); | ||
|
||
const socket = getQuizSocket(); | ||
const { data, isLoading } = useLeaderboard(socket, pinCode ?? ''); | ||
|
||
if (isLoading) { | ||
return <div>Loading...</div>; | ||
} | ||
|
||
if (data) { | ||
//TODO: 쿠키 삭제 로직 | ||
} | ||
|
||
return ( | ||
<section className="relative w-dvw min-h-dvh h-fit bg-gradient-to-br from-blue-100 via-white to-purple-100"> | ||
<ConfettiBackground /> | ||
<div className="w-full max-w-3xl mx-auto p-6 bg-gradient-to-br from-blue-50 via-white to-purple-50 rounded-2xl shadow-xl"> | ||
<span className="text-2xl font-bold mb-4 bg-gradient-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent"> | ||
최종 결과 🎉 | ||
</span> | ||
<div className="flex justify-center "> | ||
<div className="w-full"> | ||
<TopPlayer players={data?.rankerData.slice(0, 3) ?? []} /> | ||
</div> | ||
</div> | ||
{/* 랭킹 리스트 */} | ||
<PlayerList players={data?.rankerData.slice(3) ?? []} /> | ||
<div className="mt-8 p-4 bg-white/50 rounded-xl border border-gray-100"> | ||
<div className="grid grid-cols-2 gap-4 text-center"> | ||
<div className="p-2"> | ||
<div className="text-sm text-gray-500 mb-1">참가자</div> | ||
<div className="font-bold text-gray-800">{data?.participantNumber}명</div> | ||
</div> | ||
<div className="p-2 border-x border-gray-100"> | ||
<div className="text-sm text-gray-500 mb-1">평균 점수</div> | ||
<div className="font-bold text-gray-800">{data?.averageScore}점</div> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</section> | ||
); | ||
} |
18 changes: 18 additions & 0 deletions
18
packages/client/src/pages/leaderboard/model/hooks/useLeaderboard.ts
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,18 @@ | ||
import { useQuery } from '@tanstack/react-query'; | ||
import { Socket } from 'socket.io-client'; | ||
|
||
import { Ranking } from '../..'; | ||
import { emitEventWithAck } from '@/shared/utils/emitEventWithAck'; | ||
|
||
interface LeaderboardData { | ||
rankerData: Ranking[]; | ||
participantNumber: number; | ||
averageScore: number; | ||
} | ||
|
||
export const useLeaderboard = (socket: Socket, pinCode: string) => { | ||
return useQuery({ | ||
queryKey: ['leaderboard', pinCode], | ||
queryFn: () => emitEventWithAck<LeaderboardData>(socket, 'leaderboard', { pinCode }), | ||
}); | ||
}; |
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
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
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 { Socket } from 'socket.io-client'; | ||
|
||
export const emitEventWithAck = <T>(socket: Socket, event: string, data: any) => { | ||
return new Promise<T>((resolve, reject) => { | ||
socket.emit(event, data, (response: T) => { | ||
if (response) { | ||
resolve(response); | ||
} else { | ||
reject(new Error(`"${event}" event emit failed`)); | ||
} | ||
}); | ||
}); | ||
}; |
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
Oops, something went wrong.