Skip to content

Instantly share code, notes, and snippets.

@MannyGozzi
Created June 5, 2024 06:05
Show Gist options
  • Save MannyGozzi/c989c060fbaaeed0c1acbd36db8cca8f to your computer and use it in GitHub Desktop.
Save MannyGozzi/c989c060fbaaeed0c1acbd36db8cca8f to your computer and use it in GitHub Desktop.
A Practice Page Fragment for an IELTS Learning App
"use client";
import AnimatedHeader from "@/components/AnimatedHeader";
import QuestionsDropdown from "@/components/DropQ/QuestionsDropdown";
import Timer from "@/components/Timer";
import { AnimButton } from "@/components/ui/button";
import { AnimCard } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { animFadeInWithDelay, animFadeUpWithDelay } from "@/constants/anim";
import {
dynamicFillBlankContent,
multipleChoiceQuestions,
questionOptions,
readingQuestions,
} from "@/constants/mockData";
import { ContentType } from "@/contracts/types";
import { motion } from "framer-motion";
import { useEffect, useState } from "react";
import QuestionsMultipleChoice from "@/components/MCQ/QuestionsMultipleChoice";
import ExerciseHeader from "@/components/ExerciseHeader";
import WritingQuestion from "@/components/WritingQuestion";
import { countWords } from "@/app/helpers/WordCounter";
import WordCounter from "@/components/WordCount";
import { FormatType, IModuleFull, QuestionType } from "@/firebase/types";
import { getPracticeModule } from "@/firebase/getPracticeModule";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import QuestionsFillBlank from "@/components/QuestionsFillBlank";
import {
getSectionType,
getQuestionRange,
getRangeSlice,
getReadingIndex,
} from "@/app/helpers/ModuleHelpers";
export interface PracticePageProps {
params: {
id: string;
};
}
const exerciseType: ContentType = "reading";
const ReadingPage = ({ params }: PracticePageProps) => {
const { id } = params;
/* PRACTICE STATE */
const [module, setModule] = useState<IModuleFull | undefined>(undefined);
const [questionType, setQuestionType] = useState<QuestionType | undefined>(
undefined,
);
const [sectionType, setSectionType] = useState<FormatType | undefined>(
undefined,
);
const [sectionIndex, setSectionIndex] = useState(0);
const [questionRange, setQuestionRange] = useState<[number, number]>([0, 1]);
const [isDialogOpen, setIsDialogOpen] = useState(false);
/* WRITING STATE */
const [text, setText] = useState("");
const [wordCount, setWordCount] = useState(0);
const [wordLimit, setWordLimit] = useState(10);
const onWritingChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newText = e.target.value;
setText(newText);
setWordCount(countWords(newText));
};
/* Question format answer stores */
const [responses, setResponses] = useState<string[][]>([]);
useEffect(() => {
getPracticeModule(id).then((module) => {
setModule(module);
setQuestionType(module.p_type as QuestionType);
setQuestionRange(getQuestionRange(module.p_sections[0]));
setSectionType(getSectionType(module.p_sections[0]));
setResponses(new Array(module.p_questions.length).fill([]));
});
}, [id]);
/* DEBUG HELPER */
// console.log(inputValues);
// console.log(dropdownQuestionsVal)
// console.log(MCQSValues)
const onNext = () => {
saveQuestions();
if (
module?.p_sections[sectionIndex + 1] &&
sectionIndex < module?.p_sections.length - 1
) {
setSectionType(getSectionType(module.p_sections[sectionIndex + 1]));
setQuestionRange(getQuestionRange(module?.p_sections[sectionIndex + 1]));
setSectionIndex((prev) => prev + 1);
} else {
onFinishDialog();
}
};
const onBack = () => {
saveQuestions();
if (sectionIndex > 0 && module?.p_sections[sectionIndex - 1]) {
setSectionIndex((prev) => prev - 1);
setSectionType(getSectionType(module.p_sections[sectionIndex - 1]));
setQuestionRange(getQuestionRange(module?.p_sections[sectionIndex - 1]));
}
};
const saveQuestions = () => {
console.log(responses.map((val) => val.join("|")));
// TODO SAVE DATA AND SEND TO SERVER
};
const onFinishDialog = () => {
setIsDialogOpen(true);
};
const onFinish = () => {
// NAVIGATE BACK
history.back();
};
const commonProps = {
responses,
setResponses,
questionRange: questionRange,
questions: getRangeSlice(module?.p_questions, questionRange),
options: getRangeSlice(module?.p_options, questionRange),
answers: getRangeSlice(module?.p_answers, questionRange),
};
return (
<motion.div
className="flex justify-center bg-muted/40 flex-col overflow-y-hidden"
layoutId="mainBackground"
>
<div className="flex flex-col items-center h-[100vh]">
<ExerciseHeader
currentQuestion={2}
questionRange={questionRange}
currentPart={sectionIndex + 1}
numParts={module?.p_sections.length || 1}
onNext={onNext}
onBack={onBack}
onSubmit={onFinishDialog}
/>
<div className="flex flex-col p-4 gap-4 sm:flex-row max-w-2xl h-full pb-20 w-full">
<AnimCard
className="p-6 flex flex-col rounded-2xl gap-3 flex-1 overflow-y-auto"
layoutId={`anim-card-${id}`}
>
<div className="flex flex-row justify-between items-center flex-wrap gap-x-2">
<AnimatedHeader
id={id}
title={module?.mod_title || "Loading..."}
type={exerciseType}
/>
<motion.p className="font-medium" {...animFadeInWithDelay(0.2)}>
{questionType === "writing"
? `Word Limit: ${wordLimit}`
: `Questions ${questionRange[0]}-${questionRange[1]}`}
</motion.p>
</div>
<Separator />
<motion.h2
className="text-2xl font-bold"
{...animFadeUpWithDelay(0.1)}
>
{module?.mod_title || "Loading..."}
</motion.h2>
<motion.h3
className="text-xl font-bold"
{...animFadeUpWithDelay(0.2)}
>
{module?.mod_subTitle}
</motion.h3>
<motion.p
className="reading-content text-base"
{...animFadeUpWithDelay(0.3)}
>
{module?.p_readings[
getReadingIndex(module?.p_sections[sectionIndex]) - 1
] || "Loading..."}
</motion.p>
</AnimCard>
<AnimCard
className="flex-1 p-4 flex flex-col gap-3 overflow-y-auto"
{...animFadeUpWithDelay(0.15)}
>
<div className="flex flex-row justify-between">
<Timer />
{questionType === "writing" && (
<WordCounter wordCount={wordCount} />
)}
</div>
{questionType === "writing" && (
<WritingQuestion
text={text}
handleTextChange={onWritingChange}
wordCount={wordCount}
wordLimit={wordLimit}
/>
)}
{questionType === "reading" && sectionType === "dropdown" && (
<QuestionsDropdown {...commonProps} />
)}
{questionType === "reading" && sectionType === "fill" && (
<QuestionsFillBlank {...commonProps} />
)}
{questionType === "reading" && sectionType === "mcq" && (
<QuestionsMultipleChoice {...commonProps} />
)}
<AnimButton
className="self-end m-4 py-5 px-6"
variant="secondary"
onClick={onNext}
{...animFadeUpWithDelay(0.7)}
>
Next
</AnimButton>
</AnimCard>
<AlertDialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Practice Complete</AlertDialogTitle>
<AlertDialogDescription>
{
// check if there are any unanswered questions
responses.some((val) => val.length === 0)
? `You have unanswered questions. Are you sure you want to
continue?`
: `You're about to submit your answers. Are you sure you want to
continue?`
}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setIsDialogOpen(false)}>
Cancel
</AlertDialogCancel>
<AlertDialogAction onClick={onFinish}>
Continue
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
</motion.div>
);
};
export default ReadingPage;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment