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
7 changes: 1 addition & 6 deletions client/src/components/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,7 @@ export function ProtectedPage({ children, requiredRoles }: ProtectedPageProps) {
);
case "authorized":
return (
<Sidebar
role={userRole.toLowerCase() as Role}
isShowBreadcrumb={userRole !== Role.STUDENT}
>
{children}
</Sidebar>
<Sidebar role={userRole.toLowerCase() as Role}>{children}</Sidebar>
);
default:
return <WaitingLoader />;
Expand Down
3 changes: 1 addition & 2 deletions client/src/components/ui/Quiz/comp-start.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,7 @@ export function CompStart({
});
window.alert("Your answers have been submitted successfully.");
// refresh the page to get the latest data
window.location.reload();
router.push("/");
window.location.href = "/quiz";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you remove the comment here since it's no longer relevant pls

}
console.log("Final answers:", userAnswers);
}
Expand Down
31 changes: 28 additions & 3 deletions client/src/components/ui/Quiz/quiz-intro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,24 @@ interface Props {
onStart: () => void;
quizName: string;
startTime: string | Date;
timeWindow: number;
quizDuration: number;
numberOfQuestions: number;
isFinished: boolean;
isEntryClosed: boolean;
isSubmitted: boolean;
}

export default function QuizIntro({
onStart,
quizName,
startTime,
timeWindow,
quizDuration,
numberOfQuestions,
isFinished,
isEntryClosed,
isSubmitted,
}: Partial<Props>) {
let headingStyle = `text-xl sm:text-2xl md:text-3xl text-slate-800 font-bold`;
let generalInstructions =
Expand All @@ -34,6 +42,9 @@ export default function QuizIntro({
className="flex-row gap-1 font-bold"
/>
</div>
<div className="my-4 flex gap-2">
Competition will allow entering within {timeWindow} minutes
</div>
<div className="mb-2 flex items-center justify-between">
<h2 className={headingStyle}>Individual Quiz</h2>
<h2 className={headingStyle}>{quizDuration} minutes</h2>
Expand All @@ -48,9 +59,23 @@ export default function QuizIntro({
<span className="font-bold"> not </span> be expected to be to scale.
</p>
<div className="h-4 w-full"></div>
<Button size="lg" onClick={onStart}>
Start
</Button>
{isFinished ? (
<Button size="lg" variant={"inactive"}>
Competition has finished
</Button>
) : isEntryClosed ? (
<Button size="lg" variant={"inactive"}>
Entries for the competition are now closed
</Button>
) : isSubmitted ? (
<Button size="lg" variant={"inactive"}>
You have submitted your answer
</Button>
) : (
<Button size="lg" onClick={onStart}>
Start
</Button>
)}
</div>
</div>
);
Expand Down
21 changes: 18 additions & 3 deletions client/src/components/ui/mobilenav.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { AlignJustify, X } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { Drawer } from "vaul";

import { Button } from "@/components/ui/button";
import { LoginModal } from "@/components/ui/Users/login-modal";
import { useAuth } from "@/context/auth-provider";

export default function MobileNav() {
const router = useRouter();
const { isLoggedIn } = useAuth();
return (
<Drawer.Root direction="top">
<Drawer.Trigger className="relative flex h-10 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded-full bg-white px-4 text-sm font-medium shadow-sm transition-all hover:bg-[#FAFAFA] dark:bg-[#161615] dark:text-white dark:hover:bg-[#1A1A19]">
Expand Down Expand Up @@ -42,15 +46,26 @@ export default function MobileNav() {
<Link href="/awards">Awards</Link>
<Link href="/quiz">Quizzes</Link>
<Link href="/contact">Contact us</Link>
<LoginModal>
{isLoggedIn ? (
<Button
variant={"outline"}
size={"lg"}
className="border-2 border-black font-roboto text-lg"
onClick={() => router.push("/dashboard")}
>
Login
Dashboard
</Button>
</LoginModal>
) : (
<LoginModal>
<Button
variant={"outline"}
size={"lg"}
className="border-2 border-black font-roboto text-lg"
>
Login
</Button>
</LoginModal>
)}
</Drawer.Description>
{/* </div> */}
</div>
Expand Down
57 changes: 54 additions & 3 deletions client/src/pages/quiz/competition/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,18 @@ import {
CompetitionSlot,
QuizAttempt,
QuizAttemptResponse,
QuizState,
} from "@/types/quiz";

export default function CompetitionQuizPage() {
const router = useRouter();
const compId = router.query.id as string;
const [start, setStart] = useState(false);
const [isFinished, setIsFinished] = useState(false);
const [isEntryClosed, setIsEntryClosed] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
const handleStart = () => {
setStart(true);
// console.log(compId);
};

const {
Expand All @@ -40,17 +43,65 @@ export default function CompetitionQuizPage() {
enabled: !!compId, // Only run the query if compId is defined
});

if (!compId) return <WaitingLoader />;
const {
data: quizAttemptData,
isLoading: isQuizAttemptDataLoading,
error: quizAttemptDataError,
} = useFetchData<{
results: QuizAttempt[];
count: number;
next: string | null;
previous: string | null;
}>({
queryKey: ["quizAttemptList"],
endpoint: `/quiz/quiz-attempts/`,
});

const { userId } = useAuth();
useEffect(() => {
if (compData) {
const filteredAttempt = quizAttemptData?.results?.filter(
(attempt) =>
attempt.student_user_id === userId &&
attempt.quiz === compData.id &&
(attempt.state === QuizState.SUBMITTED ||
attempt.state === QuizState.COMPLETED),
);
if (filteredAttempt?.length === 1) setIsSubmitted(true);
}
}, [quizAttemptData, compData]);

useEffect(() => {
if (compData) {
const endTime = new Date(compData.open_time_date);
endTime.setMinutes(endTime.getMinutes() + compData.time_limit);
const endWindowTime = new Date(compData.open_time_date);
endWindowTime.setMinutes(
endWindowTime.getMinutes() + compData.time_window,
);
const now = new Date();
if (now > endTime) {
setIsFinished(true);
} else if (now > endWindowTime) {
setIsEntryClosed(true);
}
}
}, [compData]);

if (!compId || !quizAttemptData) return <WaitingLoader />;

console.log(compId);
if (!start) {
return (
<QuizIntro
{...{
quizName: compData?.name,
quizDuration: compData?.time_limit,
startTime: compData?.open_time_date,
timeWindow: compData?.time_window,
onStart: handleStart,
isFinished: isFinished,
isEntryClosed: isEntryClosed,
isSubmitted: isSubmitted,
}}
/>
);
Expand Down
21 changes: 12 additions & 9 deletions client/src/pages/quiz/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import Cookies from "js-cookie";
import { createContext, useContext, useEffect } from "react";

import { PublicPage } from "@/components/layout";
Expand Down Expand Up @@ -41,22 +42,24 @@ const QuizPage = () => {
endpoint: "/quiz/competition/",
});

const userRole = Cookies.get("user_role");
if (isQuizDataLoading || !quizData || isCompQuizDataLoading || !compQuizData)
return <WaitingLoader />;
if (isQuizDataError) return <div>Error Quiz: {QuizDataError?.message}</div>;
if (isCompQuizDataError)
if (userRole && isCompQuizDataError)
return <div>Error Competition: {compQuizDataError?.message}</div>;

return (
<div className="md:grid-cols mx-1 my-20 mt-10 grid auto-rows-min gap-4">
{compQuizData.results.length > 0 ? (
<CompetitionCard
className="mx-auto w-full max-w-lg"
{...compQuizData}
/>
) : (
<NoCompetitionCard className="mx-auto w-full max-w-lg" />
)}
{userRole &&
(compQuizData.results.length > 0 ? (
<CompetitionCard
className="mx-auto w-full max-w-lg"
{...compQuizData}
/>
) : (
<NoCompetitionCard className="mx-auto w-full max-w-lg" />
))}
<PracticeCard className="mx-auto w-full max-w-lg" {...quizData} />
</div>
);
Expand Down
13 changes: 13 additions & 0 deletions client/src/pages/team/submit-success.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useRouter } from "next/router";

import { Button } from "@/components/ui/button";

export default function SubmitSuccess() {
const router = useRouter();
return (
<>
<div>Team submission completed!</div>
<Button onClick={() => router.push("/team")}>Back</Button>
</>
);
}
2 changes: 2 additions & 0 deletions client/src/types/quiz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,10 @@ export interface QuizAttempt {
time_finish: Date;
time_modified: Date;
total_marks: number;
dead_line: Date;
quiz: number;
student: number;
student_user_id: number;
team: number;
}

Expand Down
16 changes: 11 additions & 5 deletions server/api/quiz/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Quiz(models.Model):
visible (BooleanField): Notes whether the quiz is visible.
open_time_date (DateTimeField): Notes when the quiz opens.
time_limit (Integer): Denotes the time allotted for each quiz.
time_window: The amount of time after quiz start that a student has to be able to start the quiz
"""

id = models.AutoField(primary_key=True)
Expand All @@ -42,9 +43,11 @@ def __str__(self):
def clean(self):
if self.is_comp:
if self.open_time_date is None:
raise ValidationError({'open_time_date': 'This field is required for competition quizzes.'})
raise ValidationError(
{'open_time_date': 'This field is required for competition quizzes.'})
if self.time_window is None:
raise ValidationError({'time_window': 'This field is required for competition quizzes.'})
raise ValidationError(
{'time_window': 'This field is required for competition quizzes.'})
super().clean()

def save(self, *args, **kwargs):
Expand All @@ -64,7 +67,8 @@ class QuizSlot(models.Model):
"""

id = models.AutoField(primary_key=True)
quiz = models.ForeignKey(Quiz, on_delete=models.CASCADE, related_name="quiz_slots")
quiz = models.ForeignKey(
Quiz, on_delete=models.CASCADE, related_name="quiz_slots")
question = models.ForeignKey(
Question, on_delete=models.CASCADE, default=None, related_name="slots"
)
Expand Down Expand Up @@ -99,7 +103,8 @@ class State(models.IntegerChoices):
COMPLETED = 4

id = models.AutoField(primary_key=True)
quiz = models.ForeignKey(Quiz, on_delete=models.CASCADE, related_name="attempts")
quiz = models.ForeignKey(
Quiz, on_delete=models.CASCADE, related_name="attempts")
student = models.ForeignKey(
Student,
on_delete=models.CASCADE,
Expand All @@ -108,7 +113,8 @@ class State(models.IntegerChoices):
null=True,
)
current_page = models.IntegerField()
state = models.IntegerField(choices=State.choices, default=State.UNATTEMPTED)
state = models.IntegerField(
choices=State.choices, default=State.UNATTEMPTED)
time_start = models.DateTimeField(auto_now_add=True)
time_finish = models.DateTimeField(null=True, blank=True)
time_modified = models.DateTimeField(auto_now=True)
Expand Down
8 changes: 6 additions & 2 deletions server/api/quiz/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ class QuizSlotSerializer(serializers.ModelSerializer):
required=True,
allow_null=True,
)
slot_index = serializers.IntegerField(required=True, min_value=0, max_value=99)
slot_index = serializers.IntegerField(
required=True, min_value=0, max_value=99)
quiz_id = serializers.PrimaryKeyRelatedField(
queryset=Quiz.objects.all(),
write_only=True,
Expand All @@ -55,7 +56,8 @@ def validate(self, data):
quiz = data["quiz"]
slot_index = data["slot_index"]
if QuizSlot.objects.filter(quiz=quiz, slot_index=slot_index).exists():
raise serializers.ValidationError("Slot index already exists for this quiz")
raise serializers.ValidationError(
"Slot index already exists for this quiz")
return data

class Meta:
Expand Down Expand Up @@ -100,6 +102,8 @@ class Meta:
class QuizAttemptSerializer(serializers.ModelSerializer):
current_page = serializers.IntegerField(default=0, required=False)
total_marks = serializers.IntegerField(default=0, required=False)
student_user_id = serializers.IntegerField(
source='student.user.id', read_only=True)

class Meta:
model = QuizAttempt
Expand Down
4 changes: 2 additions & 2 deletions server/api/quiz/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
)

router = DefaultRouter()
router.register(r"admin-quizzes", AdminQuizViewSet, basename="")
router.register(r"all-quizzes", QuizViewSet)
router.register(r"admin-quizzes", AdminQuizViewSet, basename="") # all quizzes
router.register(r"all-quizzes", QuizViewSet) # all visible quizzes
router.register(r"competition", CompetitionQuizViewSet, basename="competition")
router.register(r"quiz-slots", QuizSlotViewSet)
router.register(r"quiz-attempts", QuizAttemptViewSet)
Expand Down
Loading