Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions quizeek/src/app/(app)/auth/quiz/[quizId]/edit/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
const Page = async () => {
return <p>Quiz edit</p>;
import QuizForm from '@/components/quiz/quiz-form/quiz-form';
import { getEditableQuiz } from '@/db/queries';

type PageParams = {
params: Promise<{
quizId: string;
}>;
};

const Page = async ({ params }: PageParams) => {
const editableQuiz = await getEditableQuiz((await params).quizId);

return <QuizForm editableQuiz={editableQuiz} />;
};

export default Page;
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const QuizActivateDialog = ({ quiz }: QuizActivateDialogProps) => {
return (
<Dialog>
<DialogTrigger asChild>
<Button>Activate</Button>
<Button className="w-20">Activate</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
Expand Down
28 changes: 25 additions & 3 deletions quizeek/src/components/quiz/detail/quiz-detail-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import ActionButton from '@/components/action-button';
import { SignInButton } from '@/components/sign-in-button';
import { Button } from '@/components/ui/button';
import { QuizWithUser } from '@/db/schema/quiz';
import { useCreateQuizAttemptMutation } from '@/hooks';
import { useCreateQuizAttemptMutation, useDeleteQuizMutation } from '@/hooks';
import { User } from 'next-auth';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { redirect, useRouter } from 'next/navigation';
import { toast } from 'sonner';

import { QuizActivateDialog } from './quiz-activate-dialog';
Expand All @@ -24,6 +24,8 @@ export const QuizDetailActions = ({ user, quiz }: QuizDetailActionsProps) => {

const createQuizAttempt = useCreateQuizAttemptMutation();

const deleteQuizMutation = useDeleteQuizMutation();

const takeQuiz = async () => {
await createQuizAttempt.mutateAsync(quiz.id, {
onSuccess: async (attemptId: string) => {
Expand All @@ -35,6 +37,18 @@ export const QuizDetailActions = ({ user, quiz }: QuizDetailActionsProps) => {
});
};

const deleteQuiz = async () => {
await deleteQuizMutation.mutateAsync(quiz.id, {
onSuccess: async () => {
toast.success('Successfully deleted quiz');
redirect('/auth/profile');
},
onError: (e) => {
toast.error(e.message);
},
});
};

return (
<>
{!user && (
Expand All @@ -44,9 +58,17 @@ export const QuizDetailActions = ({ user, quiz }: QuizDetailActionsProps) => {
{!quiz.isActive && isOwner && (
<div className="self-end ml-auto flex flex-col md:flex-row gap-2">
<Link href={`/auth/quiz/${quiz.id}/edit`}>
<Button>Edit</Button>
<Button className="w-20">Edit</Button>
</Link>
<QuizActivateDialog quiz={quiz} />
<ActionButton
className="w-20"
variant="destructive"
onClick={deleteQuiz}
isLoading={deleteQuizMutation.isPending}
>
Delete
</ActionButton>
</div>
)}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Card, CardContent, CardTitle } from '@/components/ui/card';
import { getMyQuizAttempts } from '@/db/queries/quiz-attempt';
import { getMyQuizAttempts } from '@/db/queries';
import { QuizWithUser } from '@/db/schema/quiz';
import { toLocalDateTimeString } from '@/utils/date';
import { SquareArrowRight } from 'lucide-react';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Card, CardContent, CardTitle } from '@/components/ui/card';
import { getOtherQuizAttempts } from '@/db/queries/quiz-attempt';
import { getOtherQuizAttempts } from '@/db/queries';
import { QuizWithUser } from '@/db/schema/quiz';
import { toLocalDateTimeString } from '@/utils/date';
import { CircleUserRound } from 'lucide-react';
Expand Down
35 changes: 29 additions & 6 deletions quizeek/src/components/quiz/quiz-form/quiz-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ import {
AccordionTrigger,
} from '@/components/ui/accordion';
import { Form } from '@/components/ui/form';
import { QuizForm as QuizFormData, quizFormSchema } from '@/db/schema/quiz';
import {
EditableQuiz,
QuizForm as QuizFormData,
quizFormSchema,
} from '@/db/schema/quiz';
import { useSubmitQuizFormMutation } from '@/hooks/quiz';
import { toQuizDuration } from '@/utils';
import { useUploadThing } from '@/utils/uploadthing';
import { zodResolver } from '@hookform/resolvers/zod';
import { redirect } from 'next/navigation';
Expand All @@ -20,15 +25,33 @@ import { toast } from 'sonner';
import QuizInfoFormPart from './quiz-info-form-part';
import QuizQuestionsFormPart from './quiz-questions-form-part';

const QuizForm = () => {
type QuizFormProps = {
editableQuiz?: EditableQuiz;
};

const QuizForm = ({ editableQuiz }: QuizFormProps) => {
const [files, setFiles] = useState<File[]>([]);

const form = useForm<QuizFormData>({
resolver: zodResolver(quizFormSchema),
defaultValues: {
title: '',
isActive: false,
questions: [],
title: editableQuiz?.title ?? '',
description: editableQuiz?.description ?? '',
duration: toQuizDuration(editableQuiz?.timeLimitSeconds),
isActive: editableQuiz?.isActive ?? false,
imageUrl: editableQuiz?.imageUrl ?? undefined,
questions:
editableQuiz?.questions.map((q) => ({
id: q.id,
text: q.text ?? '',
number: q.number,
choices: q.choices.map((c) => ({
id: c.id,
text: c.text,
points: c.points,
isCorrect: c.isCorrect,
})),
})) ?? [],
},
});

Expand All @@ -42,7 +65,7 @@ const QuizForm = () => {

const onSubmit = async (data: QuizFormData) => {
await submitQuizFormMutation.mutateAsync(
{ data, file: files[0], startUpload },
{ id: editableQuiz?.id, data, file: files[0], startUpload },
{
onSuccess: async (quizId) => {
toast.success('Successfully submitted form');
Expand Down
16 changes: 11 additions & 5 deletions quizeek/src/components/quiz/quiz-form/quiz-questions-form-part.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ const QuizQuestionsFormPart = () => {
key: string,
value: T
) => {
setQuestions(
setQuestions((questions) =>
questions.map((q) =>
q.id === questionId
? {
Expand Down Expand Up @@ -227,14 +227,20 @@ const QuizQuestionsFormPart = () => {
decimalScale={0}
placeholder="Points"
value={choice.points}
onChange={(e) =>
onChange={(e) => {
updateChoice(
question.id,
choice.id,
'points',
parseInt(e.currentTarget.value)
)
}
);
updateChoice(
question.id,
choice.id,
'isCorrect',
parseInt(e.currentTarget.value) > 0
);
}}
/>
<span className="text-destructive md:absolute md:-bottom-5">
{
Expand All @@ -249,7 +255,7 @@ const QuizQuestionsFormPart = () => {
<Switch
id={`${question.id}-choice-${choice.id}-isCorrect`}
name={`${question.id}-choice-${choice.id}-isCorrect`}
value={choice.text}
checked={choice.isCorrect}
onCheckedChange={(v) =>
updateChoice(
question.id,
Expand Down
2 changes: 1 addition & 1 deletion quizeek/src/db/queries/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// export * from './answer';
// export * from './choice';
// export * from './question';
// export * from './quiz-attempt';
// export * from './user';
export * from './quiz';
export * from './quiz-attempt';
9 changes: 5 additions & 4 deletions quizeek/src/db/queries/quiz-attempt.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { auth } from '@/auth';
import { handleError } from '@/utils';
import { and, eq, getTableColumns, lt, ne, sql } from 'drizzle-orm';

import { db } from '..';
Expand Down Expand Up @@ -35,8 +36,8 @@ export const getMyQuizAttempts = async (
.orderBy(sql`${quizAttempts.timestamp} desc`);

return myQuizAttempts;
} catch {
throw new Error('Failed to load quiz attempts.');
} catch (error) {
throw handleError(error);
}
};

Expand Down Expand Up @@ -67,7 +68,7 @@ export const getOtherQuizAttempts = async (
.orderBy(sql`${quizAttempts.timestamp} desc`);

return otherQuizAttempts;
} catch {
throw new Error('Failed to load quiz attempts.');
} catch (error) {
throw handleError(error);
}
};
48 changes: 39 additions & 9 deletions quizeek/src/db/queries/quiz.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
'use server';

import { auth } from '@/auth';
import { InvalidSessionError } from '@/models';
import { handleError } from '@/utils';
import { and, eq, getTableColumns, like, or } from 'drizzle-orm';

import { db } from '..';
import { quizes, QuizWithUser } from '../schema/quiz';
import { EditableQuiz, quizes, QuizWithUser } from '../schema/quiz';
import { quizAttempts } from '../schema/quiz-attempt';
import { users } from '../schema/user';

Expand All @@ -24,8 +26,8 @@ export const getActiveQuizes = async (
});

return dbQuizes;
} catch {
throw new Error('Failed to load quizes.');
} catch (error) {
throw handleError(error);
}
};

Expand All @@ -39,8 +41,8 @@ export const getQuizById = async (
});

return quiz;
} catch {
throw new Error('Failed to load quiz.');
} catch (error) {
throw handleError(error);
}
};

Expand All @@ -64,8 +66,8 @@ export const getMyQuizes = async (
});

return dbQuizes;
} catch {
throw new Error('Failed to load quizes.');
} catch (error) {
throw handleError(error);
}
};

Expand Down Expand Up @@ -97,7 +99,35 @@ export const getTakenQuizes = async (
);

return dbQuizes;
} catch {
throw new Error('Failed to load quizes.');
} catch (error) {
throw handleError(error);
}
};

export const getEditableQuiz = async (id: string): Promise<EditableQuiz> => {
try {
const session = await auth();

const dbQuiz = await db.query.quizes.findFirst({
where: and(
eq(quizes.id, id),
eq(quizes.createdBy, session?.user.id ?? '')
),
with: {
questions: {
with: {
choices: true,
},
},
},
});

if (!dbQuiz) {
throw new InvalidSessionError('User is not creator of the quiz');
}

return dbQuiz;
} catch (error) {
throw handleError(error);
}
};
4 changes: 3 additions & 1 deletion quizeek/src/db/schema/quiz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { v7 as uuid } from 'uuid';
import { z } from 'zod';

import { questions } from './question';
import { questions, QuestionWithChoices } from './question';
import { quizAttempts } from './quiz-attempt';
import { User, users } from './user';

Expand All @@ -30,6 +30,8 @@ export type Quiz = InferSelectModel<typeof quizes>;

export type QuizWithUser = Quiz & { creator: User };

export type EditableQuiz = Quiz & { questions: QuestionWithChoices[] };

export const quizFormSchema = z.object({
title: z
.string({ required_error: 'Title can not be empty' })
Expand Down
9 changes: 8 additions & 1 deletion quizeek/src/hooks/quiz.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { SubmitQuizFormMutationType } from '@/models';
import { activateQuizAction, submitQuizFormAction } from '@/server-actions';
import { deleteQuizAction } from '@/server-actions/quiz/delete-quiz-action';
import { useMutation } from '@tanstack/react-query';

export const useSubmitQuizFormMutation = () =>
useMutation({
mutationFn: async ({
id,
data,
file,
startUpload,
Expand All @@ -18,11 +20,16 @@ export const useSubmitQuizFormMutation = () =>
}
}

return await submitQuizFormAction(data);
return await submitQuizFormAction(data, id);
},
});

export const useActivateQuizMutation = () =>
useMutation({
mutationFn: activateQuizAction,
});

export const useDeleteQuizMutation = () =>
useMutation({
mutationFn: deleteQuizAction,
});
1 change: 1 addition & 0 deletions quizeek/src/models/quiz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { QuizForm } from '@/db/schema/quiz';
import { ClientUploadedFileData } from 'uploadthing/types';

export type SubmitQuizFormMutationType = {
id?: string;
data: QuizForm;
file: File;
startUpload: (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { auth } from '@/auth';
import { db } from '@/db';
import { quizAttempts } from '@/db/schema/quiz-attempt';
import { handleServerActionError } from '@/utils';
import { handleError } from '@/utils';

export const createQuizAttemptAction = async (
quizId: string
Expand All @@ -18,6 +18,6 @@ export const createQuizAttemptAction = async (

return createdQuiz[0].id;
} catch (error) {
return handleServerActionError(error);
throw handleError(error);
}
};
Loading
Loading