Enabled a way for students to do assigned tasks
This commit is contained in:
@@ -4,7 +4,7 @@ import {BsArrowRepeat} from "react-icons/bs";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
color?: "rose" | "purple" | "red";
|
||||
color?: "rose" | "purple" | "red" | "green";
|
||||
variant?: "outline" | "solid";
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
@@ -24,6 +24,11 @@ export default function Button({
|
||||
onClick,
|
||||
}: Props) {
|
||||
const colorClassNames: {[key in typeof color]: {[key in typeof variant]: string}} = {
|
||||
green: {
|
||||
solid: "bg-mti-green-light text-white border border-mti-green-light hover:bg-mti-green disabled:text-mti-green disabled:bg-mti-green-ultralight selection:bg-mti-green-dark",
|
||||
outline:
|
||||
"bg-transparent text-mti-green-light border border-mti-green-light hover:bg-mti-green-light disabled:text-mti-green disabled:bg-mti-green-ultralight disabled:border-none selection:bg-mti-green-dark hover:text-white selection:text-white",
|
||||
},
|
||||
purple: {
|
||||
solid: "bg-mti-purple-light text-white border border-mti-purple-light hover:bg-mti-purple disabled:text-mti-purple disabled:bg-mti-purple-ultralight selection:bg-mti-purple-dark",
|
||||
outline:
|
||||
|
||||
@@ -3,12 +3,17 @@ import ProgressBar from "@/components/Low/ProgressBar";
|
||||
import ProfileSummary from "@/components/ProfileSummary";
|
||||
import useAssignments from "@/hooks/useAssignments";
|
||||
import useStats from "@/hooks/useStats";
|
||||
import {Assignment} from "@/interfaces/results";
|
||||
import {User} from "@/interfaces/user";
|
||||
import {MODULE_ARRAY} from "@/utils/moduleUtils";
|
||||
import useExamStore from "@/stores/examStore";
|
||||
import {getExamById} from "@/utils/exams";
|
||||
import {MODULE_ARRAY, sortByModule} from "@/utils/moduleUtils";
|
||||
import {averageScore, groupBySession} from "@/utils/stats";
|
||||
import clsx from "clsx";
|
||||
import {capitalize} from "lodash";
|
||||
import moment from "moment";
|
||||
import Link from "next/link";
|
||||
import {useRouter} from "next/router";
|
||||
import {BsArrowRepeat, BsBook, BsFileEarmarkText, BsHeadphones, BsMegaphone, BsPen, BsPencil, BsStar} from "react-icons/bs";
|
||||
|
||||
interface Props {
|
||||
@@ -19,6 +24,35 @@ export default function StudentDashboard({user}: Props) {
|
||||
const {stats} = useStats(user.id);
|
||||
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assignees: user?.id});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const setExams = useExamStore((state) => state.setExams);
|
||||
const setShowSolutions = useExamStore((state) => state.setShowSolutions);
|
||||
const setUserSolutions = useExamStore((state) => state.setUserSolutions);
|
||||
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
|
||||
const setAssignment = useExamStore((state) => state.setAssignment);
|
||||
|
||||
const startAssignment = (assignment: Assignment) => {
|
||||
const examPromises = assignment.exams.map((e) => getExamById(e.module, e.id));
|
||||
|
||||
Promise.all(examPromises).then((exams) => {
|
||||
if (exams.every((x) => !!x)) {
|
||||
setUserSolutions([]);
|
||||
setShowSolutions(false);
|
||||
setExams(exams.map((x) => x!).sort(sortByModule));
|
||||
setSelectedModules(
|
||||
exams
|
||||
.map((x) => x!)
|
||||
.sort(sortByModule)
|
||||
.map((x) => x!.module),
|
||||
);
|
||||
setAssignment(assignment);
|
||||
|
||||
router.push("/exercises");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProfileSummary
|
||||
@@ -65,7 +99,12 @@ export default function StudentDashboard({user}: Props) {
|
||||
.filter((a) => moment(a.endDate).isSameOrAfter(moment()))
|
||||
.sort((a, b) => moment(a.startDate).diff(b.startDate))
|
||||
.map((assignment) => (
|
||||
<div className="border border-mti-gray-anti-flash rounded-xl flex flex-col gap-6 p-4 min-w-[300px]" key={assignment.id}>
|
||||
<div
|
||||
className={clsx(
|
||||
"border border-mti-gray-anti-flash rounded-xl flex flex-col gap-6 p-4 min-w-[300px]",
|
||||
assignment.results.map((r) => r.user).includes(user.id) && "border-mti-green-light",
|
||||
)}
|
||||
key={assignment.id}>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="font-semibold text-xl text-mti-black/90">{assignment.name}</h3>
|
||||
<span className="flex gap-1 justify-between">
|
||||
@@ -120,11 +159,21 @@ export default function StudentDashboard({user}: Props) {
|
||||
<Button
|
||||
disabled={moment(assignment.startDate).isAfter(moment())}
|
||||
className="w-full max-w-[50%] h-full !rounded-xl -md:hidden"
|
||||
onClick={() => startAssignment(assignment)}
|
||||
variant="outline">
|
||||
Start
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{assignment.results.map((r) => r.user).includes(user.id) && (
|
||||
<Button
|
||||
onClick={() => router.push("/record")}
|
||||
color="green"
|
||||
className="w-full max-w-[50%] h-full !rounded-xl -md:hidden"
|
||||
variant="outline">
|
||||
Submitted
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -62,6 +62,7 @@ export interface Stat {
|
||||
solutions: any[];
|
||||
type: string;
|
||||
timeSpent?: number;
|
||||
assignment?: string;
|
||||
score: {
|
||||
correct: number;
|
||||
total: number;
|
||||
|
||||
@@ -41,6 +41,7 @@ export default function ExamPage({page}: Props) {
|
||||
const [userSolutions, setUserSolutions] = useExamStore((state) => [state.userSolutions, state.setUserSolutions]);
|
||||
const [showSolutions, setShowSolutions] = useExamStore((state) => [state.showSolutions, state.setShowSolutions]);
|
||||
const [selectedModules, setSelectedModules] = useExamStore((state) => [state.selectedModules, state.setSelectedModules]);
|
||||
const assignment = useExamStore((state) => state.assignment);
|
||||
|
||||
const {user} = useUser({redirectTo: "/login"});
|
||||
const router = useRouter();
|
||||
@@ -98,6 +99,7 @@ export default function ExamPage({page}: Props) {
|
||||
module: solution.module!,
|
||||
user: user?.id || "",
|
||||
date: new Date().getTime(),
|
||||
...(assignment ? {assignment: assignment.id} : {}),
|
||||
}));
|
||||
|
||||
axios
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type {NextApiRequest, NextApiResponse} from "next";
|
||||
import {app} from "@/firebase";
|
||||
import {getFirestore, collection, getDocs, query, where, doc, setDoc, addDoc} from "firebase/firestore";
|
||||
import {getFirestore, collection, getDocs, query, where, doc, setDoc, addDoc, getDoc} from "firebase/firestore";
|
||||
import {withIronSessionApiRoute} from "iron-session/next";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import {Stat} from "@/interfaces/user";
|
||||
import {Assignment} from "@/interfaces/results";
|
||||
import {groupBy} from "lodash";
|
||||
|
||||
const db = getFirestore(app);
|
||||
|
||||
@@ -42,5 +44,29 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||
const stats = req.body as Stat[];
|
||||
await stats.forEach(async (stat) => await addDoc(collection(db, "stats"), stat));
|
||||
|
||||
const groupedStatsByAssignment = groupBy(
|
||||
stats.filter((x) => !!x.assignment),
|
||||
"assignment",
|
||||
);
|
||||
if (Object.keys(groupedStatsByAssignment).length > 0) {
|
||||
const assignments = Object.keys(groupedStatsByAssignment);
|
||||
|
||||
await assignments.forEach(async (assignmentId) => {
|
||||
const assignmentStats = groupedStatsByAssignment[assignmentId] as Stat[];
|
||||
|
||||
const assignmentSnapshot = await getDoc(doc(db, "assignments", assignmentId));
|
||||
await setDoc(
|
||||
doc(db, "assignments", assignmentId),
|
||||
{
|
||||
results: [
|
||||
...(assignmentSnapshot.data() as Assignment).results,
|
||||
{user: req.session.user?.id, type: req.session.user?.focus, stats: assignmentStats},
|
||||
],
|
||||
},
|
||||
{merge: true},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json({ok: true});
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import {BsBook, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs";
|
||||
import Select from "react-select";
|
||||
import useGroups from "@/hooks/useGroups";
|
||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
||||
import useAssignments from "@/hooks/useAssignments";
|
||||
|
||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||
const user = req.session.user;
|
||||
@@ -57,6 +58,7 @@ export default function History({user}: {user: User}) {
|
||||
const [statsUserId, setStatsUserId] = useState<string | undefined>(user.id);
|
||||
const [groupedStats, setGroupedStats] = useState<{[key: string]: Stat[]}>();
|
||||
const [filter, setFilter] = useState<"months" | "weeks" | "days">();
|
||||
const {assignments} = useAssignments({});
|
||||
|
||||
const {users} = useUsers();
|
||||
const {stats, isLoading: isStatsLoading} = useStats(statsUserId);
|
||||
@@ -147,6 +149,8 @@ export default function History({user}: {user: User}) {
|
||||
const correct = dateStats.reduce((accumulator, current) => accumulator + current.score.correct, 0);
|
||||
const total = dateStats.reduce((accumulator, current) => accumulator + current.score.total, 0);
|
||||
const aggregatedScores = aggregateScoresByModule(dateStats).filter((x) => x.total > 0);
|
||||
const assignmentID = dateStats.reduce((_, current) => current.assignment as any, "");
|
||||
const assignment = assignments.find((a) => a.id === assignmentID);
|
||||
|
||||
const aggregatedLevels = aggregatedScores.map((x) => ({
|
||||
module: x.module,
|
||||
@@ -196,24 +200,33 @@ export default function History({user}: {user: User}) {
|
||||
{(aggregatedLevels.reduce((accumulator, current) => accumulator + current.level, 0) / aggregatedLevels.length).toFixed(1)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-2 place-items-start w-full -md:mt-2">
|
||||
{aggregatedLevels.map(({module, level}) => (
|
||||
<div
|
||||
key={module}
|
||||
className={clsx(
|
||||
"flex gap-2 items-center w-fit text-white -md:px-4 xl:px-4 md:px-2 py-2 rounded-xl",
|
||||
module === "reading" && "bg-ielts-reading",
|
||||
module === "listening" && "bg-ielts-listening",
|
||||
module === "writing" && "bg-ielts-writing",
|
||||
module === "speaking" && "bg-ielts-speaking",
|
||||
)}>
|
||||
{module === "reading" && <BsBook className="w-4 h-4" />}
|
||||
{module === "listening" && <BsHeadphones className="w-4 h-4" />}
|
||||
{module === "writing" && <BsPen className="w-4 h-4" />}
|
||||
{module === "speaking" && <BsMegaphone className="w-4 h-4" />}
|
||||
<span className="text-sm">{level.toFixed(1)}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="w-full flex flex-col gap-1">
|
||||
<div className="grid grid-cols-4 gap-2 place-items-start w-full -md:mt-2">
|
||||
{aggregatedLevels.map(({module, level}) => (
|
||||
<div
|
||||
key={module}
|
||||
className={clsx(
|
||||
"flex gap-2 items-center w-fit text-white -md:px-4 xl:px-4 md:px-2 py-2 rounded-xl",
|
||||
module === "reading" && "bg-ielts-reading",
|
||||
module === "listening" && "bg-ielts-listening",
|
||||
module === "writing" && "bg-ielts-writing",
|
||||
module === "speaking" && "bg-ielts-speaking",
|
||||
)}>
|
||||
{module === "reading" && <BsBook className="w-4 h-4" />}
|
||||
{module === "listening" && <BsHeadphones className="w-4 h-4" />}
|
||||
{module === "writing" && <BsPen className="w-4 h-4" />}
|
||||
{module === "speaking" && <BsMegaphone className="w-4 h-4" />}
|
||||
<span className="text-sm">{level.toFixed(1)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{assignment && (
|
||||
<span className="font-light text-sm">
|
||||
Assignment: {assignment.name}, Teacher: {users.find((u) => u.id === assignment.assigner)?.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {Module} from "@/interfaces";
|
||||
import {Exam, UserSolution} from "@/interfaces/exam";
|
||||
import {Assignment} from "@/interfaces/results";
|
||||
import {create} from "zustand";
|
||||
|
||||
export interface ExamState {
|
||||
@@ -8,11 +9,13 @@ export interface ExamState {
|
||||
showSolutions: boolean;
|
||||
hasExamEnded: boolean;
|
||||
selectedModules: Module[];
|
||||
assignment?: Assignment;
|
||||
setHasExamEnded: (hasExamEnded: boolean) => void;
|
||||
setUserSolutions: (userSolutions: UserSolution[]) => void;
|
||||
setExams: (exams: Exam[]) => void;
|
||||
setShowSolutions: (showSolutions: boolean) => void;
|
||||
setSelectedModules: (modules: Module[]) => void;
|
||||
setAssignment: (assignment: Assignment) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
@@ -22,6 +25,7 @@ export const initialState = {
|
||||
showSolutions: false,
|
||||
selectedModules: [],
|
||||
hasExamEnded: false,
|
||||
assignment: undefined,
|
||||
};
|
||||
|
||||
const useExamStore = create<ExamState>((set) => ({
|
||||
@@ -31,6 +35,7 @@ const useExamStore = create<ExamState>((set) => ({
|
||||
setShowSolutions: (showSolutions: boolean) => set(() => ({showSolutions})),
|
||||
setSelectedModules: (modules: Module[]) => set(() => ({selectedModules: modules})),
|
||||
setHasExamEnded: (hasExamEnded: boolean) => set(() => ({hasExamEnded})),
|
||||
setAssignment: (assignment: Assignment) => set(() => ({assignment})),
|
||||
reset: () => set(() => initialState),
|
||||
}));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user