Skip to content

Commit

Permalink
[FE - #74] 진행자 퀴즈 진행 페이지 UI 추가 (#79)
Browse files Browse the repository at this point in the history
* feat: recharts 설치

* feat: 총제출 수를 표현하는 그래프 컴포넌트

* feat: 최근 제출 답안 컴포넌트 추가

* feat: 통계 카드 추가

* feat: 마스터 퀴즈 세션 추가
  • Loading branch information
dooohun authored Nov 19, 2024
1 parent f1a09e8 commit d6ded0d
Show file tree
Hide file tree
Showing 41 changed files with 946 additions and 4 deletions.
395 changes: 395 additions & 0 deletions .pnp.cjs

Large diffs are not rendered by default.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1 change: 1 addition & 0 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.27.0",
"recharts": "^2.13.3",
"socket.io-client": "^4.8.1"
},
"devDependencies": {
Expand Down
4 changes: 3 additions & 1 deletion packages/client/src/app/routes/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import QuizQuestion from '@/pages/quiz-question';
import QnA from '@/pages/qna';
import GuestQnA from '@/pages/guest-qna';
import QuizList from '@/pages/quiz-list';
import QuizMasterSession from '@/pages/quiz-master-session';

export default function Router() {
return (
Expand All @@ -23,12 +24,13 @@ export default function Router() {
<Route path="/questions" element={<QnA />} />
</Route>
<Route element={<GuestLayout />}>
<Route path="/quiz/session" element={<QuizSession />} />
<Route path="/quiz/session/:id" element={<QuizSession />} />
<Route path="/quiz/wait/:pinCode" element={<QuizWait />} />
<Route path="/nickname/:pinCode" element={<Nickname />} />
<Route path="/guest/questions" element={<GuestQnA />} />
</Route>
<Route path="/quiz/question" element={<QuizQuestion />} />
<Route path="quiz/host/session/:quizId" element={<QuizMasterSession />} />
<Route path={'*'} element={<NotFound />} />
</Routes>
);
Expand Down
113 changes: 113 additions & 0 deletions packages/client/src/pages/quiz-master-session/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { useParams } from 'react-router-dom';
import StatisticsCard from './ui/StatisticsCard';

import { useEffect, useState } from 'react';
import ProgressBar from '@/shared/ui/progress-bar/ProgressBar';
import { CustomButton } from '@/shared/ui/buttons';
import AnswerGraph from '@/pages/quiz-master-session/ui/AnswerChart';
import RecentSubmittedAnswers from './ui/RecentSubmittedAnswers';

interface AnswerStat {
answer: string;
count: number;
color: string;
}

const statisticsCardItems = [
{
title: '총 제출',
value: 35,
unit: '명',
color: 'text-green-500',
subDescription: '+22명 남음',
},
{
title: '정답률',
value: 65,
unit: '%',
color: 'text-blue-500',
subDescription: '평균 대비 +5%',
},
{
title: '평균 풀이 시간',
value: 5,
unit: '초',
color: 'text-orange-500',
subDescription: '목표 시간 내 해결',
},
{
title: '평균 정답률',
value: 60,
unit: '%',
color: 'text-purple-500',
subDescription: '목표 85% 미달성',
},
];

const limitedTime = 20;

export default function QuizMasterSession() {
const { quizId } = useParams();

const [answerStats, setAnswerStats] = useState<AnswerStat[]>([
{ answer: '1번', count: 10, color: '#3B82F6' },
{ answer: '2번', count: 20, color: '#F87171' },
{ answer: '3번', count: 15, color: '#34D399' },
{ answer: '4번', count: 25, color: '#FBBF24' },
]);

const [time, setTime] = useState<number | string>(limitedTime);

const tick = () => {
setTime((prev) => {
if (typeof prev === 'string') return '종료';
if (prev === 0) return '종료';
return prev - 1;
});
// 랜덤 데이터 생성
setAnswerStats((prev) => {
return prev.map((item) => {
return {
...item,
count: Math.floor(Math.random() * 100),
};
});
});
};

useEffect(() => {
const timer = setInterval(() => {
tick();
}, 1000);
return () => clearInterval(timer);
}, []);
return (
<div className="w-screen min-h-screen">
<div className="p-5">
<div className="flex justify-between">
<div>
<h1 className="text-xl font-bold mb-2">실시간 통계</h1>
<p className="text-2xl font-bold mb-2">{quizId}Q. 퀴즈 제목</p>
</div>
<div>
<p className="font-bold text-gray-500 mb-2">제한 시간 {time}</p>
<div className="mb-2">
<CustomButton label="다음 퀴즈" type="full" />
</div>
</div>
</div>

<ProgressBar time={limitedTime} type="info" />
</div>
<div className="grid grid-cols-4 gap-6 mb-8 mx-5">
{statisticsCardItems.map((item) => (
<StatisticsCard key={item.title} {...item} />
))}
</div>
<div className="grid grid-cols-[3fr_1fr] gap-4 mx-5">
<AnswerGraph answerStats={answerStats} />
<RecentSubmittedAnswers answerStats={answerStats} />
</div>
</div>
);
}
35 changes: 35 additions & 0 deletions packages/client/src/pages/quiz-master-session/ui/AnswerChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
ResponsiveContainer,
BarChart,
CartesianGrid,
XAxis,
YAxis,
Tooltip,
Bar,
Legend,
} from 'recharts';

interface AnswerStat {
answer: string;
count: number;
color: string;
}

interface AnswerStatProps {
answerStats: AnswerStat[];
}

export default function AnswerGraph({ answerStats }: AnswerStatProps) {
return (
<ResponsiveContainer width="100%" height="100%">
<BarChart data={answerStats} barSize={60}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="answer" axisLine={false} tickLine={false} />
<YAxis axisLine={false} tickLine={false} tickCount={6} />
<Tooltip />
<Legend />
<Bar dataKey="count" fill="primary" fillOpacity={0.8} />
</BarChart>
</ResponsiveContainer>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useEffect, useState } from 'react';

interface AnswerStat {
answer: string;
count: number;
color: string;
}

// TODO: 실시간으로 변경되는 데이터를 받아야 함
interface RecentSubmittedAnswersProps {
answerStats: AnswerStat[];
}

const MAX_RECENT_SUBMITTED_ANSWERS = 8;
export default function RecentSubmittedAnswers({ answerStats }: RecentSubmittedAnswersProps) {
const [time, setTime] = useState(0);

const tick = () => {
setTime((prev) => prev + 1);
};

useEffect(() => {
const timer = setInterval(tick, 1000);
return () => clearInterval(timer);
}, []);
return (
<div>
<div className="col-span-2 bg-white rounded-xl shadow-sm border border-gray-100">
<div className="p-4 border-b border-gray-100">
<h3 className="font-semibold">최근 제출 답안</h3>
</div>
<div className="divide-y">
{[...Array(MAX_RECENT_SUBMITTED_ANSWERS)].map((_, i) => (
<div
key={i}
className="p-4 flex items-center justify-between hover:bg-gray-50 transition-colors"
>
<div className="flex items-center space-x-4">
<div
className="w-8 h-8 rounded-full flex items-center justify-center text-white font-medium"
style={{
backgroundColor: answerStats[Math.floor(Math.random() * 4)].color,
}}
>
{String.fromCharCode(65 + Math.floor(Math.random() * 4))}
</div>
<div>
<p className="font-medium">학생 {i + 1}</p>
<p className="text-sm text-gray-500">{time}초 전 제출</p>
</div>
</div>
<div className="flex items-center space-x-4">
{/* 제출 시 걸리는 시간 */}
<span className="px-2 py-1 rounded-full text-sm bg-gray-100 text-gray-600">
45초
</span>
</div>
</div>
))}
</div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
interface StatisticsCardProps {
title: string;
value: number;
unit: string;
color: string;
subDescription: string;
}

export default function StatisticsCard({
title,
value,
unit,
color,
subDescription,
}: StatisticsCardProps) {
return (
<div className="bg-white rounded-xl p-6 shadow-sm border border-gray-100">
<div className="flex justify-between items-center">
<div>
<p className="text-gray-500 text-md">{title}</p>
<p className="text-2xl font-bold">{`${value}${unit}`}</p>
<p className={`${color} text-md`}>{subDescription}</p>
</div>
</div>
</div>
);
}
Loading

0 comments on commit d6ded0d

Please sign in to comment.