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 {
|
interface Props {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
color?: "rose" | "purple" | "red";
|
color?: "rose" | "purple" | "red" | "green";
|
||||||
variant?: "outline" | "solid";
|
variant?: "outline" | "solid";
|
||||||
className?: string;
|
className?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@@ -24,6 +24,11 @@ export default function Button({
|
|||||||
onClick,
|
onClick,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const colorClassNames: {[key in typeof color]: {[key in typeof variant]: string}} = {
|
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: {
|
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",
|
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:
|
outline:
|
||||||
|
|||||||
@@ -3,12 +3,17 @@ import ProgressBar from "@/components/Low/ProgressBar";
|
|||||||
import ProfileSummary from "@/components/ProfileSummary";
|
import ProfileSummary from "@/components/ProfileSummary";
|
||||||
import useAssignments from "@/hooks/useAssignments";
|
import useAssignments from "@/hooks/useAssignments";
|
||||||
import useStats from "@/hooks/useStats";
|
import useStats from "@/hooks/useStats";
|
||||||
|
import {Assignment} from "@/interfaces/results";
|
||||||
import {User} from "@/interfaces/user";
|
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 {averageScore, groupBySession} from "@/utils/stats";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {capitalize} from "lodash";
|
import {capitalize} from "lodash";
|
||||||
import moment from "moment";
|
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";
|
import {BsArrowRepeat, BsBook, BsFileEarmarkText, BsHeadphones, BsMegaphone, BsPen, BsPencil, BsStar} from "react-icons/bs";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -19,6 +24,35 @@ export default function StudentDashboard({user}: Props) {
|
|||||||
const {stats} = useStats(user.id);
|
const {stats} = useStats(user.id);
|
||||||
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assignees: 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<ProfileSummary
|
<ProfileSummary
|
||||||
@@ -65,7 +99,12 @@ export default function StudentDashboard({user}: Props) {
|
|||||||
.filter((a) => moment(a.endDate).isSameOrAfter(moment()))
|
.filter((a) => moment(a.endDate).isSameOrAfter(moment()))
|
||||||
.sort((a, b) => moment(a.startDate).diff(b.startDate))
|
.sort((a, b) => moment(a.startDate).diff(b.startDate))
|
||||||
.map((assignment) => (
|
.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">
|
<div className="flex flex-col gap-1">
|
||||||
<h3 className="font-semibold text-xl text-mti-black/90">{assignment.name}</h3>
|
<h3 className="font-semibold text-xl text-mti-black/90">{assignment.name}</h3>
|
||||||
<span className="flex gap-1 justify-between">
|
<span className="flex gap-1 justify-between">
|
||||||
@@ -120,11 +159,21 @@ export default function StudentDashboard({user}: Props) {
|
|||||||
<Button
|
<Button
|
||||||
disabled={moment(assignment.startDate).isAfter(moment())}
|
disabled={moment(assignment.startDate).isAfter(moment())}
|
||||||
className="w-full max-w-[50%] h-full !rounded-xl -md:hidden"
|
className="w-full max-w-[50%] h-full !rounded-xl -md:hidden"
|
||||||
|
onClick={() => startAssignment(assignment)}
|
||||||
variant="outline">
|
variant="outline">
|
||||||
Start
|
Start
|
||||||
</Button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export interface Stat {
|
|||||||
solutions: any[];
|
solutions: any[];
|
||||||
type: string;
|
type: string;
|
||||||
timeSpent?: number;
|
timeSpent?: number;
|
||||||
|
assignment?: string;
|
||||||
score: {
|
score: {
|
||||||
correct: number;
|
correct: number;
|
||||||
total: number;
|
total: number;
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export default function ExamPage({page}: Props) {
|
|||||||
const [userSolutions, setUserSolutions] = useExamStore((state) => [state.userSolutions, state.setUserSolutions]);
|
const [userSolutions, setUserSolutions] = useExamStore((state) => [state.userSolutions, state.setUserSolutions]);
|
||||||
const [showSolutions, setShowSolutions] = useExamStore((state) => [state.showSolutions, state.setShowSolutions]);
|
const [showSolutions, setShowSolutions] = useExamStore((state) => [state.showSolutions, state.setShowSolutions]);
|
||||||
const [selectedModules, setSelectedModules] = useExamStore((state) => [state.selectedModules, state.setSelectedModules]);
|
const [selectedModules, setSelectedModules] = useExamStore((state) => [state.selectedModules, state.setSelectedModules]);
|
||||||
|
const assignment = useExamStore((state) => state.assignment);
|
||||||
|
|
||||||
const {user} = useUser({redirectTo: "/login"});
|
const {user} = useUser({redirectTo: "/login"});
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -98,6 +99,7 @@ export default function ExamPage({page}: Props) {
|
|||||||
module: solution.module!,
|
module: solution.module!,
|
||||||
user: user?.id || "",
|
user: user?.id || "",
|
||||||
date: new Date().getTime(),
|
date: new Date().getTime(),
|
||||||
|
...(assignment ? {assignment: assignment.id} : {}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
axios
|
axios
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
import type {NextApiRequest, NextApiResponse} from "next";
|
import type {NextApiRequest, NextApiResponse} from "next";
|
||||||
import {app} from "@/firebase";
|
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 {withIronSessionApiRoute} from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import {Stat} from "@/interfaces/user";
|
import {Stat} from "@/interfaces/user";
|
||||||
|
import {Assignment} from "@/interfaces/results";
|
||||||
|
import {groupBy} from "lodash";
|
||||||
|
|
||||||
const db = getFirestore(app);
|
const db = getFirestore(app);
|
||||||
|
|
||||||
@@ -42,5 +44,29 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const stats = req.body as Stat[];
|
const stats = req.body as Stat[];
|
||||||
await stats.forEach(async (stat) => await addDoc(collection(db, "stats"), 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});
|
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 Select from "react-select";
|
||||||
import useGroups from "@/hooks/useGroups";
|
import useGroups from "@/hooks/useGroups";
|
||||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
||||||
|
import useAssignments from "@/hooks/useAssignments";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||||
const user = req.session.user;
|
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 [statsUserId, setStatsUserId] = useState<string | undefined>(user.id);
|
||||||
const [groupedStats, setGroupedStats] = useState<{[key: string]: Stat[]}>();
|
const [groupedStats, setGroupedStats] = useState<{[key: string]: Stat[]}>();
|
||||||
const [filter, setFilter] = useState<"months" | "weeks" | "days">();
|
const [filter, setFilter] = useState<"months" | "weeks" | "days">();
|
||||||
|
const {assignments} = useAssignments({});
|
||||||
|
|
||||||
const {users} = useUsers();
|
const {users} = useUsers();
|
||||||
const {stats, isLoading: isStatsLoading} = useStats(statsUserId);
|
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 correct = dateStats.reduce((accumulator, current) => accumulator + current.score.correct, 0);
|
||||||
const total = dateStats.reduce((accumulator, current) => accumulator + current.score.total, 0);
|
const total = dateStats.reduce((accumulator, current) => accumulator + current.score.total, 0);
|
||||||
const aggregatedScores = aggregateScoresByModule(dateStats).filter((x) => x.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) => ({
|
const aggregatedLevels = aggregatedScores.map((x) => ({
|
||||||
module: x.module,
|
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)}
|
{(aggregatedLevels.reduce((accumulator, current) => accumulator + current.level, 0) / aggregatedLevels.length).toFixed(1)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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">
|
<div className="grid grid-cols-4 gap-2 place-items-start w-full -md:mt-2">
|
||||||
{aggregatedLevels.map(({module, level}) => (
|
{aggregatedLevels.map(({module, level}) => (
|
||||||
<div
|
<div
|
||||||
@@ -215,6 +221,13 @@ export default function History({user}: {user: User}) {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</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 {Module} from "@/interfaces";
|
||||||
import {Exam, UserSolution} from "@/interfaces/exam";
|
import {Exam, UserSolution} from "@/interfaces/exam";
|
||||||
|
import {Assignment} from "@/interfaces/results";
|
||||||
import {create} from "zustand";
|
import {create} from "zustand";
|
||||||
|
|
||||||
export interface ExamState {
|
export interface ExamState {
|
||||||
@@ -8,11 +9,13 @@ export interface ExamState {
|
|||||||
showSolutions: boolean;
|
showSolutions: boolean;
|
||||||
hasExamEnded: boolean;
|
hasExamEnded: boolean;
|
||||||
selectedModules: Module[];
|
selectedModules: Module[];
|
||||||
|
assignment?: Assignment;
|
||||||
setHasExamEnded: (hasExamEnded: boolean) => void;
|
setHasExamEnded: (hasExamEnded: boolean) => void;
|
||||||
setUserSolutions: (userSolutions: UserSolution[]) => void;
|
setUserSolutions: (userSolutions: UserSolution[]) => void;
|
||||||
setExams: (exams: Exam[]) => void;
|
setExams: (exams: Exam[]) => void;
|
||||||
setShowSolutions: (showSolutions: boolean) => void;
|
setShowSolutions: (showSolutions: boolean) => void;
|
||||||
setSelectedModules: (modules: Module[]) => void;
|
setSelectedModules: (modules: Module[]) => void;
|
||||||
|
setAssignment: (assignment: Assignment) => void;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,6 +25,7 @@ export const initialState = {
|
|||||||
showSolutions: false,
|
showSolutions: false,
|
||||||
selectedModules: [],
|
selectedModules: [],
|
||||||
hasExamEnded: false,
|
hasExamEnded: false,
|
||||||
|
assignment: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const useExamStore = create<ExamState>((set) => ({
|
const useExamStore = create<ExamState>((set) => ({
|
||||||
@@ -31,6 +35,7 @@ const useExamStore = create<ExamState>((set) => ({
|
|||||||
setShowSolutions: (showSolutions: boolean) => set(() => ({showSolutions})),
|
setShowSolutions: (showSolutions: boolean) => set(() => ({showSolutions})),
|
||||||
setSelectedModules: (modules: Module[]) => set(() => ({selectedModules: modules})),
|
setSelectedModules: (modules: Module[]) => set(() => ({selectedModules: modules})),
|
||||||
setHasExamEnded: (hasExamEnded: boolean) => set(() => ({hasExamEnded})),
|
setHasExamEnded: (hasExamEnded: boolean) => set(() => ({hasExamEnded})),
|
||||||
|
setAssignment: (assignment: Assignment) => set(() => ({assignment})),
|
||||||
reset: () => set(() => initialState),
|
reset: () => set(() => initialState),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user