diff --git a/src/components/Low/Button.tsx b/src/components/Low/Button.tsx index 7b5bef7e..29d529e2 100644 --- a/src/components/Low/Button.tsx +++ b/src/components/Low/Button.tsx @@ -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: diff --git a/src/dashboards/Student.tsx b/src/dashboards/Student.tsx index b0584382..7cbbedbf 100644 --- a/src/dashboards/Student.tsx +++ b/src/dashboards/Student.tsx @@ -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 ( <> moment(a.endDate).isSameOrAfter(moment())) .sort((a, b) => moment(a.startDate).diff(b.startDate)) .map((assignment) => ( -
+
r.user).includes(user.id) && "border-mti-green-light", + )} + key={assignment.id}>

{assignment.name}

@@ -120,11 +159,21 @@ export default function StudentDashboard({user}: Props) { )} + {assignment.results.map((r) => r.user).includes(user.id) && ( + + )}
))} diff --git a/src/interfaces/user.ts b/src/interfaces/user.ts index 39c34479..513d3ae7 100644 --- a/src/interfaces/user.ts +++ b/src/interfaces/user.ts @@ -62,6 +62,7 @@ export interface Stat { solutions: any[]; type: string; timeSpent?: number; + assignment?: string; score: { correct: number; total: number; diff --git a/src/pages/(exam)/ExamPage.tsx b/src/pages/(exam)/ExamPage.tsx index 425fade7..424cc806 100644 --- a/src/pages/(exam)/ExamPage.tsx +++ b/src/pages/(exam)/ExamPage.tsx @@ -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 diff --git a/src/pages/api/stats/index.ts b/src/pages/api/stats/index.ts index 618aed16..7444b50d 100644 --- a/src/pages/api/stats/index.ts +++ b/src/pages/api/stats/index.ts @@ -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}); } diff --git a/src/pages/record.tsx b/src/pages/record.tsx index fbb65fa1..85d40294 100644 --- a/src/pages/record.tsx +++ b/src/pages/record.tsx @@ -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(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)}
-
- {aggregatedLevels.map(({module, level}) => ( -
- {module === "reading" && } - {module === "listening" && } - {module === "writing" && } - {module === "speaking" && } - {level.toFixed(1)} -
- ))} + +
+
+ {aggregatedLevels.map(({module, level}) => ( +
+ {module === "reading" && } + {module === "listening" && } + {module === "writing" && } + {module === "speaking" && } + {level.toFixed(1)} +
+ ))} +
+ + {assignment && ( + + Assignment: {assignment.name}, Teacher: {users.find((u) => u.id === assignment.assigner)?.name} + + )}
); diff --git a/src/stores/examStore.ts b/src/stores/examStore.ts index 1d5bc642..7561d155 100644 --- a/src/stores/examStore.ts +++ b/src/stores/examStore.ts @@ -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((set) => ({ @@ -31,6 +35,7 @@ const useExamStore = create((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), }));