(
@@ -52,7 +61,7 @@ export const QuestionList = ({
{questions.map((question) => (
-
+
))}
diff --git a/quizeek/src/components/quiz/question/question.tsx b/quizeek/src/components/quiz/question/question.tsx
index f4c9bca..87ac24e 100644
--- a/quizeek/src/components/quiz/question/question.tsx
+++ b/quizeek/src/components/quiz/question/question.tsx
@@ -1,15 +1,22 @@
import { Card, CardContent } from '@/components/ui/card';
-import { type QuestionWithPublicChoices } from '@/db/schema/question';
+import {
+ QuestionWithChoices,
+ type QuestionWithPublicChoices,
+} from '@/db/schema/question';
+import {
+ type QuizAttempt as QuizAttemptType,
+ QuizAttemptWithAnswers,
+} from '@/db/schema/quiz-attempt';
import { MultiChoiceList } from './choice/multi-choice-list';
import { SingleChoiceList } from './choice/single-choice-list';
import { QuestionDescription } from './question-description';
-
type QuestionProps = {
- question: QuestionWithPublicChoices;
+ question: QuestionWithPublicChoices | QuestionWithChoices;
+ attempt: QuizAttemptType | QuizAttemptWithAnswers;
};
-export const Question = ({ question }: QuestionProps) => {
+export const Question = ({ question, attempt }: QuestionProps) => {
return (
@@ -21,12 +28,14 @@ export const Question = ({ question }: QuestionProps) => {
)}
{question.type === 'single_choice' && (
)}
diff --git a/quizeek/src/components/quiz/quiz-attempt.tsx b/quizeek/src/components/quiz/quiz-attempt.tsx
index 810e1c1..03ca38b 100644
--- a/quizeek/src/components/quiz/quiz-attempt.tsx
+++ b/quizeek/src/components/quiz/quiz-attempt.tsx
@@ -1,14 +1,15 @@
'use client';
import { Form } from '@/components/ui/form';
-import { QuizWithPublicQuestions } from '@/db/schema/quiz';
+import { QuizWithPublicQuestions, QuizWithQuestions } from '@/db/schema/quiz';
import {
QuizAttemptResponse,
quizAttemptResponseSchema,
type QuizAttempt as QuizAttemptType,
+ QuizAttemptWithAnswers,
} from '@/db/schema/quiz-attempt';
import { useSaveQuizAttemptMutation } from '@/hooks';
-import { useQuizTimer } from '@/hooks/quiz-timer';
+import { useQuizTimer } from '@/hooks';
import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'next/navigation';
import { useEffect, useRef } from 'react';
@@ -19,8 +20,8 @@ import { QuizAttemptSaveDialog } from './attempt/quiz-attempt-save-dialog';
import { QuestionList } from './question/question-list';
type QuizAttemptProps = {
- quiz: QuizWithPublicQuestions;
- attempt: QuizAttemptType;
+ quiz: QuizWithPublicQuestions | QuizWithQuestions;
+ attempt: QuizAttemptType | QuizAttemptWithAnswers;
};
export const QuizAttempt = ({ quiz, attempt }: QuizAttemptProps) => {
@@ -32,15 +33,31 @@ export const QuizAttempt = ({ quiz, attempt }: QuizAttemptProps) => {
attemptTimestamp: attempt.timestamp,
});
+ const getDefaultValues = () => {
+ if (attempt.type === 'base') {
+ return {
+ ...quiz.questions.reduce((acc, question) => {
+ acc[question.id] = [];
+ return acc;
+ }, {} as QuizAttemptResponse),
+ };
+ }
+
+ return {
+ ...attempt.answers.reduce((acc, answer) => {
+ acc[answer.choice.questionId] = [
+ ...(acc[answer.choice.questionId] ?? []),
+ answer.choiceId,
+ ];
+ return acc;
+ }, {} as QuizAttemptResponse),
+ };
+ };
+
const formRef = useRef(null);
const form = useForm({
resolver: zodResolver(quizAttemptResponseSchema),
- defaultValues: {
- ...quiz.questions.reduce((acc, question) => {
- acc[question.id] = [];
- return acc;
- }, {} as QuizAttemptResponse),
- },
+ defaultValues: getDefaultValues(),
});
const onSubmit = async (data: QuizAttemptResponse): Promise => {
@@ -59,25 +76,30 @@ export const QuizAttempt = ({ quiz, attempt }: QuizAttemptProps) => {
};
useEffect(() => {
- if (isTimerUp) {
+ if (isTimerUp && attempt.type === 'base') {
formRef.current?.requestSubmit();
}
- }, [isTimerUp]);
+ }, [isTimerUp, attempt]);
return (
<>
- {timer}
+
+ {attempt.type === 'base' ? timer : `${attempt.score}pts`}
+
>
diff --git a/quizeek/src/components/quiz/quiz-form/quiz-form.tsx b/quizeek/src/components/quiz/quiz-form/quiz-form.tsx
index 83621b1..d4c2d38 100644
--- a/quizeek/src/components/quiz/quiz-form/quiz-form.tsx
+++ b/quizeek/src/components/quiz/quiz-form/quiz-form.tsx
@@ -13,7 +13,7 @@ import {
QuizForm as QuizFormData,
quizFormSchema,
} from '@/db/schema/quiz';
-import { useSubmitQuizFormMutation } from '@/hooks/quiz';
+import { useSubmitQuizFormMutation } from '@/hooks';
import { toQuizDuration } from '@/utils';
import { useUploadThing } from '@/utils/uploadthing';
import { zodResolver } from '@hookform/resolvers/zod';
diff --git a/quizeek/src/db/queries/answer.ts b/quizeek/src/db/queries/answer.ts
deleted file mode 100644
index e69de29..0000000
diff --git a/quizeek/src/db/queries/choice.ts b/quizeek/src/db/queries/choice.ts
deleted file mode 100644
index e69de29..0000000
diff --git a/quizeek/src/db/queries/index.ts b/quizeek/src/db/queries/index.ts
index f9fc950..417056f 100644
--- a/quizeek/src/db/queries/index.ts
+++ b/quizeek/src/db/queries/index.ts
@@ -1,6 +1,2 @@
-// export * from './answer';
-// export * from './choice';
-// export * from './question';
-// export * from './user';
export * from './quiz';
export * from './quiz-attempt';
diff --git a/quizeek/src/db/queries/question.ts b/quizeek/src/db/queries/question.ts
deleted file mode 100644
index e69de29..0000000
diff --git a/quizeek/src/db/queries/quiz-attempt.ts b/quizeek/src/db/queries/quiz-attempt.ts
index eb223cf..aa0f0cc 100644
--- a/quizeek/src/db/queries/quiz-attempt.ts
+++ b/quizeek/src/db/queries/quiz-attempt.ts
@@ -1,12 +1,25 @@
import { auth } from '@/auth';
+import { InvalidSessionError } from '@/models';
import { handleError } from '@/utils';
-import { and, desc, eq, getTableColumns, lt, ne, sql } from 'drizzle-orm';
+import {
+ and,
+ desc,
+ eq,
+ getTableColumns,
+ isNotNull,
+ lt,
+ ne,
+ or,
+ sql,
+} from 'drizzle-orm';
+import { DateTime, Duration } from 'luxon';
import { db } from '..';
import { quizes } from '../schema/quiz';
import {
QuizAttempt,
quizAttempts,
+ QuizAttemptWithAnswers,
QuizAttemptWithUser,
} from '../schema/quiz-attempt';
import { users } from '../schema/user';
@@ -27,15 +40,30 @@ 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(desc(quizAttempts.timestamp));
- return myQuizAttempts;
+ // adding 1hr because of drizzle sqlite timestamp bug
+ return myQuizAttempts.map((a) => ({
+ ...a,
+ type: 'base',
+ timestamp:
+ DateTime.fromSQL(a.timestamp)
+ .plus(
+ Duration.fromObject({
+ hour: 1,
+ })
+ )
+ .toSQL() ?? a.timestamp,
+ }));
} catch (error) {
throw handleError(error);
}
@@ -59,15 +87,30 @@ 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,
+ type: 'base',
+ timestamp:
+ DateTime.fromSQL(a.timestamp)
+ .plus(
+ Duration.fromObject({
+ hour: 1,
+ })
+ )
+ .toSQL() ?? a.timestamp,
+ }));
} catch (error) {
throw handleError(error);
}
@@ -81,8 +124,48 @@ export const getQuizAttemptById = async (
where: eq(quizAttempts.id, attemptId),
});
- return quizAttempt;
- } catch {
- throw new Error('Failed to load quiz attempt.');
+ return quizAttempt ? { ...quizAttempt, type: 'base' } : quizAttempt;
+ } catch (error) {
+ throw handleError(error);
+ }
+};
+
+export const getQuizAttemptWithAnswers = async (
+ attemptId: string
+): Promise => {
+ try {
+ const session = await auth();
+
+ const quizAttempt = await db.query.quizAttempts.findFirst({
+ where: and(
+ eq(quizAttempts.id, attemptId),
+ eq(quizAttempts.userId, session?.user.id ?? '')
+ ),
+ with: {
+ answers: {
+ with: {
+ choice: true,
+ },
+ },
+ },
+ });
+
+ if (!quizAttempt) {
+ throw new InvalidSessionError('Attempt does not belong to the user');
+ }
+
+ return {
+ ...quizAttempt,
+ type: 'with_answers',
+ answers: quizAttempt.answers.map((a) => ({
+ ...a,
+ choice: {
+ ...a.choice,
+ type: 'private',
+ },
+ })),
+ };
+ } catch (error) {
+ throw handleError(error);
}
};
diff --git a/quizeek/src/db/queries/quiz.ts b/quizeek/src/db/queries/quiz.ts
index cda5b36..5b9bf57 100644
--- a/quizeek/src/db/queries/quiz.ts
+++ b/quizeek/src/db/queries/quiz.ts
@@ -6,11 +6,13 @@ import { handleError } from '@/utils';
import { and, desc, eq, getTableColumns, like, or } from 'drizzle-orm';
import { db } from '..';
+import { Choice, PublicChoice } from '../schema/choice';
import {
EditableQuiz,
Quiz,
quizes,
QuizWithPublicQuestions,
+ QuizWithQuestions,
QuizWithUser,
} from '../schema/quiz';
import { quizAttempts } from '../schema/quiz-attempt';
@@ -52,7 +54,7 @@ export const getQuizById = async (
}
};
-export const getQuizWithQuestionsById = async (
+export const getQuizWithPublicQuestionsById = async (
quizId: string
): Promise => {
try {
@@ -73,7 +75,50 @@ export const getQuizWithQuestionsById = async (
},
});
- return quiz;
+ return !quiz
+ ? quiz
+ : {
+ ...quiz,
+ questions: quiz.questions.map((q) => ({
+ ...q,
+ choices: q.choices.map((c) => ({
+ ...c,
+ type: 'public',
+ })) as PublicChoice[],
+ })),
+ };
+ } catch {
+ throw new Error('Failed to load quiz.');
+ }
+};
+
+export const getQuizWithQuestionsById = async (
+ quizId: string
+): Promise => {
+ try {
+ const quiz = await db.query.quizes.findFirst({
+ where: eq(quizes.id, quizId),
+ with: {
+ questions: {
+ with: {
+ choices: true,
+ },
+ },
+ },
+ });
+
+ return !quiz
+ ? quiz
+ : {
+ ...quiz,
+ questions: quiz.questions.map((q) => ({
+ ...q,
+ choices: q.choices.map((c) => ({
+ ...c,
+ type: 'private',
+ })) as Choice[],
+ })),
+ };
} catch {
throw new Error('Failed to load quiz.');
}
@@ -159,7 +204,16 @@ export const getEditableQuiz = async (id: string): Promise => {
throw new InvalidSessionError('User is not creator of the quiz');
}
- return dbQuiz;
+ return {
+ ...dbQuiz,
+ questions: dbQuiz.questions.map((q) => ({
+ ...q,
+ choices: q.choices.map((c) => ({
+ ...c,
+ type: 'private',
+ })) as Choice[],
+ })),
+ };
} catch (error) {
throw handleError(error);
}
diff --git a/quizeek/src/db/queries/user.ts b/quizeek/src/db/queries/user.ts
deleted file mode 100644
index e69de29..0000000
diff --git a/quizeek/src/db/schema/answer.ts b/quizeek/src/db/schema/answer.ts
index a3619bb..4da9307 100644
--- a/quizeek/src/db/schema/answer.ts
+++ b/quizeek/src/db/schema/answer.ts
@@ -1,8 +1,8 @@
-import { relations } from 'drizzle-orm';
+import { InferSelectModel, relations } from 'drizzle-orm';
import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { v7 as uuid } from 'uuid';
-import { choices } from './choice';
+import { Choice, choices } from './choice';
import { quizAttempts } from './quiz-attempt';
export const answers = sqliteTable('answer', {
@@ -21,3 +21,7 @@ export const answersRelations = relations(answers, ({ one }) => ({
references: [choices.id],
}),
}));
+
+type Answer = InferSelectModel;
+
+export type AnswerWithChoice = Answer & { choice: Choice };
diff --git a/quizeek/src/db/schema/choice.ts b/quizeek/src/db/schema/choice.ts
index 13a924a..c7df85f 100644
--- a/quizeek/src/db/schema/choice.ts
+++ b/quizeek/src/db/schema/choice.ts
@@ -21,6 +21,8 @@ export const choicesRelations = relations(choices, ({ one, many }) => ({
answers: many(answers),
}));
-export type Choice = InferSelectModel;
+export type Choice = InferSelectModel & { type: 'private' };
-export type PublicChoice = Omit;
+export type PublicChoice = Omit & {
+ type: 'public';
+};
diff --git a/quizeek/src/db/schema/quiz-attempt.ts b/quizeek/src/db/schema/quiz-attempt.ts
index fda194e..6d1143b 100644
--- a/quizeek/src/db/schema/quiz-attempt.ts
+++ b/quizeek/src/db/schema/quiz-attempt.ts
@@ -3,7 +3,7 @@ import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { v7 as uuid } from 'uuid';
import { z } from 'zod';
-import { answers } from './answer';
+import { answers, AnswerWithChoice } from './answer';
import { quizes } from './quiz';
import { User, users } from './user';
@@ -32,7 +32,9 @@ export const quizAttemptsRelations = relations(
})
);
-export type QuizAttempt = InferSelectModel;
+export type QuizAttempt = InferSelectModel & {
+ type: 'base';
+};
export type QuizAttemptWithUser = QuizAttempt & { user: User };
@@ -41,3 +43,8 @@ export const quizAttemptResponseSchema = z.record(
z.array(z.string())
);
export type QuizAttemptResponse = z.infer;
+
+export type QuizAttemptWithAnswers = Omit & {
+ answers: AnswerWithChoice[];
+ type: 'with_answers';
+};
diff --git a/quizeek/src/hooks/answer.ts b/quizeek/src/hooks/answer.ts
deleted file mode 100644
index e69de29..0000000
diff --git a/quizeek/src/hooks/choice.ts b/quizeek/src/hooks/choice.ts
deleted file mode 100644
index e69de29..0000000
diff --git a/quizeek/src/hooks/index.ts b/quizeek/src/hooks/index.ts
index 46c99e1..95d66fb 100644
--- a/quizeek/src/hooks/index.ts
+++ b/quizeek/src/hooks/index.ts
@@ -1,6 +1,4 @@
-// export * from './answer';
-// export * from './choice';
-// export * from './question';
+export * from './quiz-timer';
export * from './quiz-attempt';
export * from './quiz';
export * from './user';
diff --git a/quizeek/src/hooks/question.ts b/quizeek/src/hooks/question.ts
deleted file mode 100644
index e69de29..0000000
diff --git a/quizeek/src/hooks/quiz-attempt.ts b/quizeek/src/hooks/quiz-attempt.ts
index 30aea2c..6bd3372 100644
--- a/quizeek/src/hooks/quiz-attempt.ts
+++ b/quizeek/src/hooks/quiz-attempt.ts
@@ -1,7 +1,7 @@
import {
createQuizAttemptAction,
saveQuizAttemptAction,
-} from '@/server-actions/quiz-attempt';
+} from '@/server-actions';
import { useMutation } from '@tanstack/react-query';
export const useCreateQuizAttemptMutation = () =>
diff --git a/quizeek/src/hooks/quiz.ts b/quizeek/src/hooks/quiz.ts
index 81fbbc8..9ad65d2 100644
--- a/quizeek/src/hooks/quiz.ts
+++ b/quizeek/src/hooks/quiz.ts
@@ -1,6 +1,9 @@
import { SubmitQuizFormMutationType } from '@/models';
-import { activateQuizAction, submitQuizFormAction } from '@/server-actions';
-import { deleteQuizAction } from '@/server-actions/quiz/delete-quiz-action';
+import {
+ activateQuizAction,
+ deleteQuizAction,
+ submitQuizFormAction,
+} from '@/server-actions';
import { useMutation } from '@tanstack/react-query';
export const useSubmitQuizFormMutation = () =>
diff --git a/quizeek/src/server-actions/answer/index.ts b/quizeek/src/server-actions/answer/index.ts
deleted file mode 100644
index 44da958..0000000
--- a/quizeek/src/server-actions/answer/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-// TODO: Export `export * from ...;`
diff --git a/quizeek/src/server-actions/choice/index.ts b/quizeek/src/server-actions/choice/index.ts
deleted file mode 100644
index 44da958..0000000
--- a/quizeek/src/server-actions/choice/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-// TODO: Export `export * from ...;`
diff --git a/quizeek/src/server-actions/index.ts b/quizeek/src/server-actions/index.ts
index d838761..5ae5075 100644
--- a/quizeek/src/server-actions/index.ts
+++ b/quizeek/src/server-actions/index.ts
@@ -1,6 +1,3 @@
export * from './user';
export * from './quiz';
-// export * from './answer';
-// export * from './choice';
-// export * from './question';
-// export * from './quiz-attempt';
+export * from './quiz-attempt';
diff --git a/quizeek/src/server-actions/question/index.ts b/quizeek/src/server-actions/question/index.ts
deleted file mode 100644
index 44da958..0000000
--- a/quizeek/src/server-actions/question/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-// TODO: Export `export * from ...;`
diff --git a/quizeek/src/server-actions/quiz/index.ts b/quizeek/src/server-actions/quiz/index.ts
index 9e3b293..07cdd83 100644
--- a/quizeek/src/server-actions/quiz/index.ts
+++ b/quizeek/src/server-actions/quiz/index.ts
@@ -1,2 +1,3 @@
export * from './submit-quiz-form-action';
export * from './activate-quiz-action';
+export * from './delete-quiz-action';
diff --git a/quizeek/src/utils/index.ts b/quizeek/src/utils/index.ts
index db6e52c..969d8f7 100644
--- a/quizeek/src/utils/index.ts
+++ b/quizeek/src/utils/index.ts
@@ -1,3 +1,4 @@
export * from './error';
export * from './string';
export * from './number';
+export * from './date';