Enabled a way for students to do assigned tasks

This commit is contained in:
Tiago Ribeiro
2023-11-09 11:44:58 +00:00
parent c2acb39859
commit 536c1dfab3
7 changed files with 123 additions and 22 deletions

View File

@@ -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:

View File

@@ -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>
))}

View File

@@ -62,6 +62,7 @@ export interface Stat {
solutions: any[];
type: string;
timeSpent?: number;
assignment?: string;
score: {
correct: number;
total: number;

View File

@@ -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

View File

@@ -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});
}

View File

@@ -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,6 +200,8 @@ export default function History({user}: {user: User}) {
{(aggregatedLevels.reduce((accumulator, current) => accumulator + current.level, 0) / aggregatedLevels.length).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
@@ -215,6 +221,13 @@ export default function History({user}: {user: User}) {
</div>
))}
</div>
{assignment && (
<span className="font-light text-sm">
Assignment: {assignment.name}, Teacher: {users.find((u) => u.id === assignment.assigner)?.name}
</span>
)}
</div>
</>
);

View File

@@ -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),
}));