From a4521847c0902b3c414dec77bf9978c3af2da0da Mon Sep 17 00:00:00 2001 From: Dominik Borbuliak Date: Sun, 15 Dec 2024 16:12:25 +0100 Subject: [PATCH 1/6] fix: Fixed attempt lists --- quizeek/src/db/queries/quiz-attempt.ts | 56 +++++++++++++++++++++----- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/quizeek/src/db/queries/quiz-attempt.ts b/quizeek/src/db/queries/quiz-attempt.ts index 5299351..fc4c929 100644 --- a/quizeek/src/db/queries/quiz-attempt.ts +++ b/quizeek/src/db/queries/quiz-attempt.ts @@ -1,6 +1,16 @@ import { auth } from '@/auth'; import { handleError } from '@/utils'; -import { and, eq, getTableColumns, lt, ne, sql } from 'drizzle-orm'; +import { + and, + eq, + getTableColumns, + isNotNull, + lt, + ne, + or, + sql, +} from 'drizzle-orm'; +import { DateTime, Duration } from 'luxon'; import { db } from '..'; import { quizes } from '../schema/quiz'; @@ -27,15 +37,29 @@ export const getMyQuizAttempts = async ( and( eq(quizes.id, quizId), eq(quizAttempts.userId, session?.user.id ?? ''), - lt( - sql`DATETIME(quiz_attempt.date, '+' || quiz.time_limit_seconds || ' seconds')`, - sql`CURRENT_TIMESTAMP` + or( + lt( + sql`DATETIME(quiz_attempt.date, '+' || quiz.time_limit_seconds || ' seconds')`, + sql`CURRENT_TIMESTAMP` + ), + isNotNull(quizAttempts.score) ) ) ) .orderBy(sql`${quizAttempts.timestamp} desc`); - return myQuizAttempts; + // adding 1hr because of drizzle sqlite timestamp bug + return myQuizAttempts.map((a) => ({ + ...a, + timestamp: + DateTime.fromSQL(a.timestamp) + .plus( + Duration.fromObject({ + hour: 1, + }) + ) + .toSQL() ?? a.timestamp, + })); } catch (error) { throw handleError(error); } @@ -59,15 +83,29 @@ export const getOtherQuizAttempts = async ( and( eq(quizes.id, quizId), ne(quizAttempts.userId, session?.user.id ?? ''), - lt( - sql`DATETIME(quiz_attempt.date, '+' || quiz.time_limit_seconds || ' seconds')`, - sql`CURRENT_TIMESTAMP` + or( + lt( + sql`DATETIME(quiz_attempt.date, '+' || quiz.time_limit_seconds || ' seconds')`, + sql`CURRENT_TIMESTAMP` + ), + isNotNull(quizAttempts.score) ) ) ) .orderBy(sql`${quizAttempts.timestamp} desc`); - return otherQuizAttempts; + // adding 1hr because of drizzle sqlite timestamp bug + return otherQuizAttempts.map((a) => ({ + ...a, + timestamp: + DateTime.fromSQL(a.timestamp) + .plus( + Duration.fromObject({ + hour: 1, + }) + ) + .toSQL() ?? a.timestamp, + })); } catch (error) { throw handleError(error); } From e0d769eb2bb3ad110afa36c3084ade237eb63cbc Mon Sep 17 00:00:00 2001 From: Dominik Borbuliak Date: Sun, 15 Dec 2024 16:15:18 +0100 Subject: [PATCH 2/6] fix: Fixed question bubble overflow --- quizeek/src/components/quiz/question/question-bubble.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quizeek/src/components/quiz/question/question-bubble.tsx b/quizeek/src/components/quiz/question/question-bubble.tsx index 7357eef..84dfe0f 100644 --- a/quizeek/src/components/quiz/question/question-bubble.tsx +++ b/quizeek/src/components/quiz/question/question-bubble.tsx @@ -42,7 +42,7 @@ export const QuestionBubble = ({ transition, }} className={cn( - 'rounded-full border w-6 h-6 text-center hover:bg-primary cursor-pointer overflow-x-auto text-foreground', + 'rounded-full border w-6 h-6 text-center hover:bg-primary cursor-pointer text-foreground', currentQuestion === question.id && 'bg-primary dark:text-black' )} onMouseUp={onClick} From 8bc3f2badb5e2098f428ac360716aa40599b4612 Mon Sep 17 00:00:00 2001 From: Dominik Borbuliak Date: Sun, 15 Dec 2024 16:18:01 +0100 Subject: [PATCH 3/6] feat: Improved finish button in quiz attempt --- .../src/components/quiz/attempt/quiz-attempt-save-dialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quizeek/src/components/quiz/attempt/quiz-attempt-save-dialog.tsx b/quizeek/src/components/quiz/attempt/quiz-attempt-save-dialog.tsx index a401fb4..caac869 100644 --- a/quizeek/src/components/quiz/attempt/quiz-attempt-save-dialog.tsx +++ b/quizeek/src/components/quiz/attempt/quiz-attempt-save-dialog.tsx @@ -21,7 +21,7 @@ export const QuizAttemptSaveDialog = ({ }: QuizAttemptSaveDialogProps) => { return ( - + From 663ba82b10c772e0228fb678c628136e19cf350a Mon Sep 17 00:00:00 2001 From: Dominik Borbuliak Date: Sun, 15 Dec 2024 17:43:30 +0100 Subject: [PATCH 4/6] feat: Added quiz view --- .../[quizId]/attempt/[attemptId]/page.tsx | 7 ++- .../quiz/[quizId]/view/[attemptId]/page.tsx | 36 ++++++++++- .../quiz/question/choice/choice.tsx | 34 ++++++++++- .../question/choice/multi-choice-list.tsx | 13 +++- .../quiz/question/choice/multi-choice.tsx | 25 +++++++- .../question/choice/single-choice-list.tsx | 15 ++++- .../quiz/question/choice/single-choice.tsx | 26 +++++++- .../quiz/question/question-list.tsx | 15 ++++- .../src/components/quiz/question/question.tsx | 17 ++++-- quizeek/src/components/quiz/quiz-attempt.tsx | 54 ++++++++++++----- quizeek/src/db/queries/quiz-attempt.ts | 50 +++++++++++++++- quizeek/src/db/queries/quiz.ts | 60 ++++++++++++++++++- quizeek/src/db/schema/answer.ts | 8 ++- quizeek/src/db/schema/choice.ts | 6 +- quizeek/src/db/schema/quiz-attempt.ts | 11 +++- 15 files changed, 327 insertions(+), 50 deletions(-) diff --git a/quizeek/src/app/(app)/auth/quiz/[quizId]/attempt/[attemptId]/page.tsx b/quizeek/src/app/(app)/auth/quiz/[quizId]/attempt/[attemptId]/page.tsx index 44de31b..58893a6 100644 --- a/quizeek/src/app/(app)/auth/quiz/[quizId]/attempt/[attemptId]/page.tsx +++ b/quizeek/src/app/(app)/auth/quiz/[quizId]/attempt/[attemptId]/page.tsx @@ -1,5 +1,8 @@ import { QuizAttempt } from '@/components/quiz/quiz-attempt'; -import { getQuizAttemptById, getQuizWithQuestionsById } from '@/db/queries'; +import { + getQuizAttemptById, + getQuizWithPublicQuestionsById, +} from '@/db/queries'; import { notFound, redirect } from 'next/navigation'; import React from 'react'; @@ -17,7 +20,7 @@ const Page = async ({ params }: AttemptPageProps) => { notFound(); } - const quiz = await getQuizWithQuestionsById(routeParams.quizId); + const quiz = await getQuizWithPublicQuestionsById(routeParams.quizId); const attempt = await getQuizAttemptById(routeParams.attemptId); if (!quiz || !attempt) { diff --git a/quizeek/src/app/(app)/auth/quiz/[quizId]/view/[attemptId]/page.tsx b/quizeek/src/app/(app)/auth/quiz/[quizId]/view/[attemptId]/page.tsx index 3e44a7d..bc52f70 100644 --- a/quizeek/src/app/(app)/auth/quiz/[quizId]/view/[attemptId]/page.tsx +++ b/quizeek/src/app/(app)/auth/quiz/[quizId]/view/[attemptId]/page.tsx @@ -1,5 +1,37 @@ -const Page = async () => { - return

Quiz view

; +import { QuizAttempt } from '@/components/quiz/quiz-attempt'; +import { + getQuizAttemptWithAnswers, + getQuizWithQuestionsById, +} from '@/db/queries'; +import { notFound } from 'next/navigation'; + +type PageProps = { + params?: Promise<{ + quizId?: string; + attemptId?: string; + }>; +}; + +const Page = async ({ params }: PageProps) => { + const routeParams = await params; + + if (!routeParams?.quizId || !routeParams.attemptId) { + notFound(); + } + + const quiz = await getQuizWithQuestionsById(routeParams.quizId); + const attempt = await getQuizAttemptWithAnswers(routeParams.attemptId); + + if (!quiz || !attempt) { + return notFound(); + } + + return ( + <> +

{quiz.title}

+ + + ); }; export default Page; diff --git a/quizeek/src/components/quiz/question/choice/choice.tsx b/quizeek/src/components/quiz/question/choice/choice.tsx index 28577eb..aa41e43 100644 --- a/quizeek/src/components/quiz/question/choice/choice.tsx +++ b/quizeek/src/components/quiz/question/choice/choice.tsx @@ -1,11 +1,39 @@ import { Card } from '@/components/ui/card'; +import { cn } from '@/lib/utils'; import { PropsWithChildren } from 'react'; -export type ChoiceProps = PropsWithChildren; +export type ChoiceProps = { + enableHiglighting: boolean; + isCorrect: boolean; + wasSelected: boolean; +} & PropsWithChildren; -export const Choice = ({ children }: ChoiceProps) => { +export const Choice = ({ + enableHiglighting, + isCorrect, + wasSelected, + children, + ...props +}: ChoiceProps) => { return ( - + {children} ); diff --git a/quizeek/src/components/quiz/question/choice/multi-choice-list.tsx b/quizeek/src/components/quiz/question/choice/multi-choice-list.tsx index e9a0de8..022acaa 100644 --- a/quizeek/src/components/quiz/question/choice/multi-choice-list.tsx +++ b/quizeek/src/components/quiz/question/choice/multi-choice-list.tsx @@ -1,16 +1,22 @@ import { ScrollArea } from '@/components/ui/scroll-area'; import { Choice, PublicChoice } from '@/db/schema/choice'; +import { + type QuizAttempt as QuizAttemptType, + QuizAttemptWithAnswers, +} from '@/db/schema/quiz-attempt'; import { MultiChoice } from './multi-choice'; export type MultiChoiceListProps = { questionId: string; choices: (Choice | PublicChoice)[]; + attempt: QuizAttemptType | QuizAttemptWithAnswers; }; export const MultiChoiceList = ({ questionId, choices, + attempt, }: MultiChoiceListProps) => { return ( {choices.map((choice) => ( - + ))} ); diff --git a/quizeek/src/components/quiz/question/choice/multi-choice.tsx b/quizeek/src/components/quiz/question/choice/multi-choice.tsx index 62c9f70..7981a85 100644 --- a/quizeek/src/components/quiz/question/choice/multi-choice.tsx +++ b/quizeek/src/components/quiz/question/choice/multi-choice.tsx @@ -2,6 +2,10 @@ import { Checkbox } from '@/components/ui/checkbox'; import { FormControl, FormField, FormItem } from '@/components/ui/form'; import { Label } from '@/components/ui/label'; import { Choice as ChoiceType, PublicChoice } from '@/db/schema/choice'; +import { + type QuizAttempt as QuizAttemptType, + QuizAttemptWithAnswers, +} from '@/db/schema/quiz-attempt'; import { useFormContext } from 'react-hook-form'; import { Choice } from './choice'; @@ -9,13 +13,29 @@ import { Choice } from './choice'; export type MultiChoiceProps = { questionId: string; choice: ChoiceType | PublicChoice; + attempt: QuizAttemptType | QuizAttemptWithAnswers; }; -export const MultiChoice = ({ questionId, choice }: MultiChoiceProps) => { +export const MultiChoice = ({ + questionId, + choice, + attempt, +}: MultiChoiceProps) => { const form = useFormContext(); + const enableHiglighting = + attempt.type === 'with_answers' && choice.type === 'private'; + const wasSelected = + enableHiglighting && + !!attempt.answers.find((a) => a.choiceId === choice.id); + const isCorrect = enableHiglighting && choice.isCorrect; + return ( - + { { diff --git a/quizeek/src/components/quiz/question/choice/single-choice-list.tsx b/quizeek/src/components/quiz/question/choice/single-choice-list.tsx index cf0030d..698560a 100644 --- a/quizeek/src/components/quiz/question/choice/single-choice-list.tsx +++ b/quizeek/src/components/quiz/question/choice/single-choice-list.tsx @@ -2,6 +2,10 @@ import { FormControl, FormField, FormItem } from '@/components/ui/form'; import { RadioGroup } from '@/components/ui/radio-group'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Choice, PublicChoice } from '@/db/schema/choice'; +import { + type QuizAttempt as QuizAttemptType, + QuizAttemptWithAnswers, +} from '@/db/schema/quiz-attempt'; import { useFormContext } from 'react-hook-form'; import { SingleChoice } from './single-choice'; @@ -9,11 +13,13 @@ import { SingleChoice } from './single-choice'; export type SingleChoiceListProps = { questionId: string; choices: (Choice | PublicChoice)[]; + attempt: QuizAttemptType | QuizAttemptWithAnswers; }; export const SingleChoiceList = ({ questionId, choices, + attempt, }: SingleChoiceListProps) => { const form = useFormContext(); @@ -30,10 +36,15 @@ export const SingleChoiceList = ({ field.onChange([v])} - defaultValue={field.value} + value={field.value?.[0]} + defaultValue={field.value?.[0]} > {choices.map((choice) => ( - + ))} diff --git a/quizeek/src/components/quiz/question/choice/single-choice.tsx b/quizeek/src/components/quiz/question/choice/single-choice.tsx index 41bdda7..32b9c20 100644 --- a/quizeek/src/components/quiz/question/choice/single-choice.tsx +++ b/quizeek/src/components/quiz/question/choice/single-choice.tsx @@ -2,18 +2,38 @@ import { FormControl } from '@/components/ui/form'; import { Label } from '@/components/ui/label'; import { RadioGroupItem } from '@/components/ui/radio-group'; import { Choice as ChoiceType, PublicChoice } from '@/db/schema/choice'; +import { + type QuizAttempt as QuizAttemptType, + QuizAttemptWithAnswers, +} from '@/db/schema/quiz-attempt'; import { Choice } from './choice'; export type SingleChoiceProps = { choice: ChoiceType | PublicChoice; + attempt: QuizAttemptType | QuizAttemptWithAnswers; }; -export const SingleChoice = ({ choice }: SingleChoiceProps) => { +export const SingleChoice = ({ choice, attempt }: SingleChoiceProps) => { + const enableHiglighting = + attempt.type === 'with_answers' && choice.type === 'private'; + const wasSelected = + enableHiglighting && + !!attempt.answers.find((a) => a.choiceId === choice.id); + const isCorrect = enableHiglighting && choice.isCorrect; + return ( - + - +