Skip to content

Commit

Permalink
Merge pull request #82 from Learn-and-Give/develop
Browse files Browse the repository at this point in the history
Release/v1.3.0
  • Loading branch information
Choimingil authored Sep 11, 2022
2 parents bff31e2 + 2a48e14 commit d73743b
Show file tree
Hide file tree
Showing 16 changed files with 451 additions and 401 deletions.
11 changes: 11 additions & 0 deletions api/quizzes/quiz-rate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import api from "../my-api";
import { getCode } from "../session-code";
interface Props {
quizId: string;
rate: "GOOD" | "BAD" | null;
}
export const updateQuizRate = async ({ quizId, rate }: Props) => {
api.defaults.headers.common["code"] = getCode();

return await api.put(`/quizzes/${quizId}/rate`, { rate });
};
146 changes: 146 additions & 0 deletions components/quizzes/quiz/quiz-detail-nav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import Link from "next/link";
import { useRouter } from "next/router";
import React, { useState } from "react";
import useChallengeDetailQuery from "../../../hooks/challenge-detail-query";
import { classNames } from "../../../styles/classname-maker";
import { QuizSummary } from "../../../types/Quiz";
interface Props {
challengeId: string;
quizId: string;
quizzes: QuizSummary[];
}
function QuizDetailNav({ challengeId, quizId, quizzes }: Props) {
const { data: challengeDetail } = useChallengeDetailQuery({ challengeId });
const router = useRouter();

const isMyQuiz = router.asPath.search("my") !== -1;

const [showToc, setShowToc] = useState(false);

const onTocClick = () => {
setShowToc((prev) => !prev);
};

const onMoveQuizClick = (move: string) => {
if (!quizzes) {
return;
}
for (let i = 0; i < quizzes.length; i++) {
if (String(quizzes[i].quizId) === quizId) {
if (move === "prev" && i - 1 >= 0) {
document.location.replace(
`/challenges/${challengeId}/quizzes/${quizzes[i - 1].quizId}${
isMyQuiz ? "/my" : ""
}?week=${router.asPath.split("?week=")[1]}`
);
} else if (move === "next" && i + 1 < quizzes.length) {
document.location.replace(
`/challenges/${challengeId}/quizzes/${quizzes[i + 1].quizId}${
isMyQuiz ? "/my" : ""
}?week=${router.asPath.split("?week=")[1]}`
);
}
}
}
};
// 퀴즈가 첫 번째 혹은 마지막 순서인지 확인한다. 두 경우에는 각각 이전과 이후로 이동하지 못한다.
const isDisabled = (move: string) => {
if (!quizzes) {
return;
}

for (let i = 0; i < quizzes.length; i++) {
if (String(quizzes[i].quizId) === quizId) {
if (move === "prev" && i == 0) {
return true;
} else if (move === "next" && i == quizzes.length - 1) {
return true;
}
}
}
return false;
};

return (
<>
<div className="fixed flex items-center justify-between gap-3 w-full left-0 bottom-0 p-3 border-t-[1px] bg-white z-10">
<button
type="button"
onClick={onTocClick}
className="py-2 px-4 rounded bg-gray-100 text-gray-700 cursor-pointer"
>
문제 목록
</button>
<Link href={`/challenges/${challengeId}`}>
<button
type="button"
className="text-gray-700 cursor-pointer hover:drop-shadow"
>
{challengeDetail?.title} 챌린지
</button>
</Link>
<div className="flex gap-2">
<button
onClick={() => onMoveQuizClick("prev")}
className={classNames(
isDisabled("prev")
? "bg-gray-100 text-gray-700 cursor-default"
: "bg-[#5c3cde] hover:bg-[#4026ab] text-white font-bold focus:outline-none focus:shadow-outline cursor-pointer",
"py-2 px-4 rounded"
)}
>
이전
</button>
<button
onClick={() => onMoveQuizClick("next")}
className={classNames(
isDisabled("next")
? "bg-gray-200 text-gray-700 cursor-default"
: "bg-[#5c3cde] hover:bg-[#4026ab] text-white font-bold focus:outline-none focus:shadow-outline cursor-pointer",
"py-2 px-4 rounded"
)}
>
다음
</button>
</div>
</div>
{showToc && (
<div className="fixed left-0 top-0 flex flex-col gap-5 w-1/4 h-full py-32 px-10 bg-white shadow-sm border-[1px] border-gray-300 sm:w-[80%] sm:py-20 animate-in slide-in-from-left-10 duration-200">
<h2 className="font-semibold text-xl cursor-pointer">문제 목록</h2>

<ul className="flex flex-col gap-1 overflow-y-scroll scrollbar-thin">
{quizzes?.map((quiz) => (
<li
key={quiz.quizId}
className={classNames(
String(quiz.quizId) === quizId
? "text-lg font-semibold text-[#5c3cde]"
: "",
"flex items-center gap-2"
)}
onClick={() => setShowToc(false)}
>
<button
onClick={() => {
document.location.replace(
`/challenges/${challengeId}/quizzes/${quiz.quizId}${
isMyQuiz ? "/my" : ""
}?week=${router.asPath.split("?week=")[1]}`
);
}}
>
{quiz.quizTitle}
</button>
<span className="badge badge-secondary font-normal">
{quiz.quizWeek}주차
</span>
</li>
))}
</ul>
</div>
)}
</>
);
}

export default QuizDetailNav;
11 changes: 10 additions & 1 deletion components/quizzes/quiz/quiz-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ function QuizList({ quizzes }: Props) {
<th className="bg-gray-200">제목</th>
<th className="bg-gray-200">작성자</th>
<th className="bg-gray-200">주차</th>
<th className="bg-gray-200">좋아요</th>
<th className="bg-gray-200">해결</th>
</tr>
</thead>
Expand All @@ -43,7 +44,15 @@ function QuizList({ quizzes }: Props) {
<td>{quiz.quizId}</td>
<td>{quiz.quizTitle}</td>
<td>{quiz.writerName}</td>
<td>{quiz.quizWeek}</td>
<td>{quiz.quizWeek}</td>
<td className="flex items-center justify-start">
<span className="flex items-center justify-center gap-1">
<Icon icon="icon-park-solid:good-two" />
<span className="">
{quiz.cntOfGood ? quiz.cntOfGood : 0}
</span>
</span>
</td>
{quiz.solveAnswer ? (
<td>
<Icon
Expand Down
84 changes: 84 additions & 0 deletions components/quizzes/rate/quiz-rate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Icon } from "@iconify/react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import React from "react";
import { updateQuizRate } from "../../../api/quizzes/quiz-rate";
import useQuizDetailQuery from "../../../hooks/quiz-detail";
import { QuizDetailSelect } from "../../../types/Quiz";
interface Props {
quizId: string;
}
interface QuizRate {
rate: "GOOD" | "BAD" | null;
}
function QuizRate({ quizId }: Props) {
const queryClient = useQueryClient();
const { data: quizDetail } = useQuizDetailQuery();
const { mutate: mutateQuizRate } = useMutation(
({ rate }: QuizRate) => updateQuizRate({ quizId: quizId, rate }),
{
onMutate: async (data) => {
await queryClient.cancelQueries(["quizDetail", quizId]);

const previousQuizDetail = queryClient.getQueryData<QuizDetailSelect>([
"quizDetail",
quizId,
]);

if (previousQuizDetail) {
queryClient.setQueryData<QuizDetailSelect>(["quizDetail", quizId], {
...previousQuizDetail,
didIRate: data.rate,
cntOfGood:
previousQuizDetail.didIRate === "GOOD"
? previousQuizDetail.cntOfGood - 1
: data.rate === "GOOD"
? previousQuizDetail.cntOfGood + 1
: previousQuizDetail.cntOfGood,
});
}

return { previousQuizDetail };
},
onError: (err, newQuizDetail, context) => {
queryClient.setQueryData(["quizDetail", quizId], quizDetail);
},
onSettled: () => {
queryClient.invalidateQueries(["quizDetail", quizId]);
},
}
);

const onRateClick = ({ rate }: QuizRate) => {
mutateQuizRate({ rate });
};

return (
<div className="flex items-start gap-4">
<span className="flex items-center gap-1">
{quizDetail?.didIRate === "GOOD" ? (
<button onClick={() => onRateClick({ rate: null })}>
<Icon icon="icon-park-solid:good-two" height={22} />
</button>
) : (
<button onClick={() => onRateClick({ rate: "GOOD" })}>
<Icon icon="icon-park-outline:good-two" height={22} />
</button>
)}
<span className="text-sm">
{quizDetail?.cntOfGood ? quizDetail?.cntOfGood : 0}
</span>
</span>
{quizDetail?.didIRate === "BAD" ? (
<button onClick={() => onRateClick({ rate: null })} className="pt-1">
<Icon icon="icon-park-solid:bad-two" height={22} />
</button>
) : (
<button onClick={() => onRateClick({ rate: "BAD" })} className="pt-1">
<Icon icon="icon-park-outline:bad-two" height={22} />
</button>
)}
</div>
);
}

export default QuizRate;
20 changes: 20 additions & 0 deletions hooks/quiz-detail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useQuery } from "@tanstack/react-query";
import { AxiosError } from "axios";
import { useRouter } from "next/router";
import { fetchQuizDetail } from "../api/quizzes/quiz-detail";
import { QuizDetail, QuizDetailSelect } from "../types/Quiz";
export default function useQuizDetailQuery() {
const router = useRouter();
const quizId = String(router.query.qid);
return useQuery<QuizDetail, AxiosError, QuizDetailSelect>(
["quizDetail", quizId],
() => fetchQuizDetail({ quizId }),
{
enabled: !!router.query.qid,
onError: (err) => {},
select: (data) => {
return { ...data, quizRubric: JSON.parse(data.quizRubric) };
},
}
);
}
34 changes: 15 additions & 19 deletions mocks/challenges-handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,23 +174,19 @@ const challengesOpenWeeks = {
};

export const challengesHandlers = [
rest.get(formattingUrl("/challenges"), (req, res, ctx) => {
return res(ctx.status(200), ctx.json(challenges));
}),

rest.get(formattingUrl("/challenges/:cid"), (req, res, ctx) => {
return res(ctx.status(200), ctx.json(challengeDetail));
}),

rest.get(formattingUrl("/my/challenges"), (req, res, ctx) => {
return res(ctx.status(200), ctx.json(myChallenges));
}),

rest.get(formattingUrl("/challenges/:cid/now"), (req, res, ctx) => {
return res(ctx.status(200), ctx.json(currentWeek));
}),

rest.get(formattingUrl("/challenges/:cid/weeks"), (req, res, ctx) => {
return res(ctx.status(200), ctx.json(challengesOpenWeeks));
}),
// rest.get(formattingUrl("/challenges"), (req, res, ctx) => {
// return res(ctx.status(200), ctx.json(challenges));
// }),
// rest.get(formattingUrl("/challenges/:cid"), (req, res, ctx) => {
// return res(ctx.status(200), ctx.json(challengeDetail));
// }),
// rest.get(formattingUrl("/my/challenges"), (req, res, ctx) => {
// return res(ctx.status(200), ctx.json(myChallenges));
// }),
// rest.get(formattingUrl("/challenges/:cid/now"), (req, res, ctx) => {
// return res(ctx.status(200), ctx.json(currentWeek));
// }),
// rest.get(formattingUrl("/challenges/:cid/weeks"), (req, res, ctx) => {
// return res(ctx.status(200), ctx.json(challengesOpenWeeks));
// }),
];
15 changes: 7 additions & 8 deletions mocks/comments-handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,11 @@ const comments = [
];

export const commentsHandlers = [
rest.get(`${BASE_URL}/quizzes/:qid/comments`, (req, res, ctx) => {
return res(ctx.status(200), ctx.json(comments));
}),

rest.post(`${BASE_URL}/quizzes/:qid/comments`, (req, res, ctx) => {
comments.push(req.body);
return res(ctx.status(201));
}),
// rest.get(`${BASE_URL}/quizzes/:qid/comments`, (req, res, ctx) => {
// return res(ctx.status(200), ctx.json(comments));
// }),
// rest.post(`${BASE_URL}/quizzes/:qid/comments`, (req, res, ctx) => {
// comments.push(req.body);
// return res(ctx.status(201));
// }),
];
29 changes: 14 additions & 15 deletions mocks/quiz-submit-handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,18 @@ let submitCount = {
week: 0,
};
export const quizSubmitHandlers = [
rest.post(formattingUrl("/challenges/:cid/quizzes"), (req, res, ctx) => {
submitCount = {
count: 1,
challengeId: 0,
week: 0,
};
return res(ctx.status(201));
}),

rest.get(
formattingUrl("/challenges/:cid/my/quizzes/count"),
(req, res, ctx) => {
return res(ctx.status(200), ctx.json(submitCount));
}
),
// rest.post(formattingUrl("/challenges/:cid/quizzes"), (req, res, ctx) => {
// submitCount = {
// count: 1,
// challengeId: 0,
// week: 0,
// };
// return res(ctx.status(201));
// }),
// rest.get(
// formattingUrl("/challenges/:cid/my/quizzes/count"),
// (req, res, ctx) => {
// return res(ctx.status(200), ctx.json(submitCount));
// }
// ),
];
Loading

0 comments on commit d73743b

Please sign in to comment.