Skip to content

Commit

Permalink
[FE - #57] 메인페이지 UI 작성 (#58)
Browse files Browse the repository at this point in the history
* feat: 여러 문제를 관리하는 상태 추가

* feat: 메인 페이지 UI 구현

* feat: 메인 페이지 기능 추가

- 버튼 클릭 시 핀번호 검사 후 이동
- 엔터 버튼 클릭 시 검사

---------

Co-authored-by: dooohun <[email protected]>
  • Loading branch information
chan-byeong and dooohun authored Nov 14, 2024
1 parent 777cf14 commit 205e000
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 73 deletions.
3 changes: 2 additions & 1 deletion packages/FE/src/app/routes/Router.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Routes, Route } from 'react-router-dom';

import HostLayout from '@/app/layouts/HostLayout';
import MainPage from '@/pages/main';
import QuizCreatePage from '@/pages/quiz-create';
import GuestLayout from '@/app/layouts/GuestLayout';
import NotFound from '@/app/routes/NotFound';
Expand All @@ -14,7 +15,7 @@ import GuestQnA from '@/pages/guest-qna';
export default function Router() {
return (
<Routes>
<Route path="/" element={<h1>MAIN PAGE</h1>} />
<Route path="/" element={<MainPage />} />
<Route element={<HostLayout />}>
<Route path="/quiz/create" element={<QuizCreatePage />} />
<Route path="/questions" element={<QnA />} />
Expand Down
62 changes: 62 additions & 0 deletions packages/FE/src/pages/main/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { toastController } from '@/features/toast/model/toastController';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';

const useGetPinNumbers = () => {
return { data: [123456] };
};

export default function MainPage() {
const [pin, setPin] = useState<string>('');
const { data: pinNumbers } = useGetPinNumbers();

const naviagte = useNavigate();
const toast = toastController();

const handleClick = () => {
if (pinNumbers?.includes(Number(pin))) {
naviagte('nickname');
return;
}
toast.error('유효하지 않은 코드입니다');
};

const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handleClick();
}
};
return (
<section className="min-h-screen flex flex-col items-center">
<h1 className="text-6xl font-bold text-primary mt-32 mb-24">You Quiz</h1>

<div className="w-full max-w-4xl px-4">
<div className="w-full flex justify-between border-[1.5px] border-primary rounded-xl p-2">
<input
type="text"
placeholder="Join Code"
className="w-3/4 border-none outline-none p-3 bg-transparent"
value={pin}
onChange={(e) => setPin(e.target.value)}
onKeyDown={handleKeyDown}
/>
<button
className="bg-primary text-white text-md font-semibold rounded-xl py-2 px-6"
onClick={handleClick}
>
퀴즈 참가하기
</button>
</div>
</div>

<div className="mt-4">
<button className="px-6 py-2 text-primary border border-weak rounded-full hover:bg-primary-light">
로그인
</button>
<button className="px-6 py-2 text-primary border border-weak rounded-full hover:bg-primary-light">
회원가입
</button>
</div>
</section>
);
}
111 changes: 108 additions & 3 deletions packages/FE/src/pages/quiz-create/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,115 @@
import { useState } from 'react';

import { CustomButton } from '@/shared/ui/buttons';
import PlusIcon from '@/shared/assets/icons/plus.svg?react';
import QuizCreateSection from './ui/QuizCreateSection';

interface Choice {
content: string;
isCorrect: boolean;
position: number;
}

export interface QuizData {
content: string;
quizType: 'MC' | 'TF';
timeLimit: number;
point: number;
position: number;
choices: Choice[];
}

export default function QuizCreatePage() {
//TODO: 문제 추가하기 상태 관리
const [currentQuizIndex, setCurrentQuizIndex] = useState(0);
const [quizzes, setQuizzes] = useState<QuizData[]>([
{
content: '',
quizType: 'MC',
timeLimit: 30,
point: 1000,
position: 0,
choices: [
{ content: '', isCorrect: false, position: 0 },
{ content: '', isCorrect: false, position: 1 },
],
},
]);

const addNewQuiz = () => {
setQuizzes((prev) => [
...prev,
{
content: '',
quizType: 'MC',
timeLimit: 30,
point: 1000,
position: prev.length,
choices: [
{ content: '', isCorrect: false, position: 0 },
{ content: '', isCorrect: false, position: 1 },
],
},
]);
setCurrentQuizIndex((prev) => prev + 1);
};

const handlePreQuiz = () => {
if (currentQuizIndex === 0) return;
setCurrentQuizIndex((prev) => prev - 1);
};

const handleNextQuiz = () => {
if (currentQuizIndex === quizzes.length - 1) return;
setCurrentQuizIndex((prev) => prev + 1);
};

return (
<div className="flex w-full">
<QuizCreateSection />
<div className="flex flex-col w-full mt-6 mr-6">
<div className=" flex gap-4 bg-white rounded-base p-4 mb-4">
<button className="text-weak-md" onClick={handlePreQuiz}>
이전 문제
</button>
<button className="text-weak-md" onClick={handleNextQuiz}>
다음 문제
</button>
<div className="flex-1 flex justify-end text-weak-md">문제 유형</div>
</div>

<QuizCreateSection
key={currentQuizIndex}
currentQuizIndex={currentQuizIndex}
quizData={quizzes[currentQuizIndex]}
onQuizUpdate={(updatedData) => {
setQuizzes((prev) => {
const newQuizzes = [...prev];
newQuizzes[currentQuizIndex] = updatedData as QuizData;
return newQuizzes;
});
}}
/>

<div className="self-start mt-10">
<CustomButton
Icon={PlusIcon}
type="outline"
size="md"
color="primary"
label="문제 추가하기"
onClick={() => {
addNewQuiz();
console.log(quizzes);
console.log(currentQuizIndex);
}}
/>
</div>
<div className="self-end mr-6">
<CustomButton
label="퀴즈 발행하기"
onClick={() => {
console.log('퀴즈 발행하기');
}}
/>
</div>
</div>
);
}
110 changes: 44 additions & 66 deletions packages/FE/src/pages/quiz-create/ui/QuizCreateSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,83 +6,76 @@ import AnswerBox from './AnswerBox';
import TimeSelectBox from './TimeSelectBox';
import PlusIcon from '@/shared/assets/icons/plus.svg?react';

interface SelectOption {
option: string;
isAnswer: boolean;
import { QuizData } from '../index';

interface QuizCreateSectionProps {
quizData: QuizData;
currentQuizIndex: number;
onQuizUpdate: (updatedData: Partial<QuizData>) => void;
}

export default function QuizCreateSection() {
const [quizTitle, setQuizTitle] = useState('');
const [selectOptions, setSelectOptions] = useState<SelectOption[]>([
{ option: '', isAnswer: false },
{ option: '', isAnswer: false },
]);
export default function QuizCreateSection({
quizData,
currentQuizIndex,
onQuizUpdate,
}: QuizCreateSectionProps) {
const [showTimeSelect, setShowTimeSelect] = useState(false);
const [timer, setTimer] = useState(20);
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);

const toggleAnswer = (index: number) => {
setSelectOptions((prev) => {
const newOptions = prev.map((option, i) =>
i === index ? { ...option, isAnswer: !option.isAnswer } : option,
);
return newOptions;
});
};

const setOption = (index: number, value: string) => {
setSelectOptions((prev) => {
const newOptions = [...prev];
newOptions[index].option = value;
return newOptions;
});
};

const handleKeyDown = (index: number, e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
if (index < selectOptions.length - 1) {
inputRefs.current[index + 1]?.focus();
} else {
setSelectOptions((prev) => [...prev, { option: '', isAnswer: false }]);
setTimeout(() => {
inputRefs.current[index + 1]?.focus();
}, 0);
}
inputRefs.current[index + 1]?.focus();
}
};

const updateChoice = (index: number, updates: Partial<(typeof quizData.choices)[0]>) => {
const updatedChoices = quizData.choices.map((choice, i) =>
i === index ? { ...choice, ...updates } : choice,
);
onQuizUpdate({
...quizData,
choices: updatedChoices,
});
};

return (
<section className="flex-1 m-6">
<article className="min-w-content min-h-[732px] flex flex-col gap-1 items-center bg-white rounded-base p-6">
<section className="">
<article className="min-w-content min-h-[532px] flex flex-col gap-1 items-center bg-white rounded-base p-6">
<p className="self-start relative">
<span className="text-weak-md mr-3">N번 퀴즈</span>
<span className="text-weak-md mr-3">{`${currentQuizIndex + 1}번 퀴즈`}</span>
<span
className="text-weak-sm cursor-pointer"
onClick={() => setShowTimeSelect(!showTimeSelect)}
>
{timer === 0 ? '제한없음' : `${timer}초`}
{`${quizData.timeLimit}초`}
</span>
{showTimeSelect && (
<TimeSelectBox setTime={setTimer} setShowTimeSelect={setShowTimeSelect} />
<TimeSelectBox
setTime={(time: number) => onQuizUpdate({ ...quizData, timeLimit: time })}
setShowTimeSelect={setShowTimeSelect}
/>
)}
</p>
<p className="w-full">
<InputBox
placeholder="문제를 입력해주세요"
type="box"
onSubmit={(value) => setQuizTitle(value)}
onSubmit={(value) => onQuizUpdate({ ...quizData, content: value })}
/>
</p>
<div className="flex flex-col gap-4 w-full mt-10">
{selectOptions.map((_, index) => (
{quizData.choices.map((choice, index) => (
<AnswerBox
key={index}
selected={selectOptions[index].isAnswer}
answerSetter={() => toggleAnswer(index)}
optionSetter={(value: string) => setOption(index, value)}
selected={choice.isCorrect}
answerSetter={() => updateChoice(index, { isCorrect: !choice.isCorrect })}
optionSetter={(value: string) => {
updateChoice(index, { content: value });
}}
inputRef={(el) => (inputRefs.current[index] = el)}
onKeyDown={(e) => handleKeyDown(index, e)}
onKeyDown={(e) => {
handleKeyDown(index, e);
}}
/>
))}
</div>
Expand All @@ -94,29 +87,14 @@ export default function QuizCreateSection() {
color="secondary"
label="답안 추가하기"
onClick={() => {
setSelectOptions((prev) => [...prev, { option: '', isAnswer: false }]);
onQuizUpdate({
...quizData,
choices: [...quizData.choices, { content: '', isCorrect: false, position: 0 }],
});
}}
/>
</div>
<div className="self-start mt-10">
<CustomButton
Icon={PlusIcon}
type="outline"
size="md"
color="primary"
label="문제 추가하기"
onClick={() => {}}
/>
</div>
</article>

<CustomButton
label="퀴즈 발행하기"
onClick={() => {
console.log(quizTitle);
console.log('퀴즈 발행하기');
}}
/>
</section>
);
}
7 changes: 4 additions & 3 deletions packages/FE/src/pages/quiz-create/ui/TimeSelectBox.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Dispatch, SetStateAction } from 'react';

interface TimeSelectBoxProps {
setTime: Dispatch<SetStateAction<number>>;
setTime: (time: number) => void;
setShowTimeSelect: Dispatch<SetStateAction<boolean>>;
}

const TIMES = [10, 20, 30, 40, 0];
const TIMES = [10, 20, 30, 40];

export default function TimeSelectBox({ setTime, setShowTimeSelect }: TimeSelectBoxProps) {
return (
<div className="absolute w-[100px] left-6 bg-white rounded-base border overflow-hidden z-10">
Expand All @@ -19,7 +20,7 @@ export default function TimeSelectBox({ setTime, setShowTimeSelect }: TimeSelect
}}
className="cursor-pointer text-weak-md text-center border-b py-1"
>
{time === 0 ? '제한없음' : `${time}초`}
{`${time}초`}
</li>
))}
</ul>
Expand Down

0 comments on commit 205e000

Please sign in to comment.