diff --git a/src/constants/ielts.tsx b/src/constants/ielts.tsx index ec150744..296131ae 100644 --- a/src/constants/ielts.tsx +++ b/src/constants/ielts.tsx @@ -1,5 +1,7 @@ import {Module} from "@/interfaces"; +export const MODULES: Module[] = ["reading", "listening", "writing", "speaking"]; + export const BAND_SCORES: {[key in Module]: number[]} = { reading: [0, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9], listening: [0, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9], diff --git a/src/pages/api/stats/update.ts b/src/pages/api/stats/update.ts new file mode 100644 index 00000000..2b895a1b --- /dev/null +++ b/src/pages/api/stats/update.ts @@ -0,0 +1,100 @@ +import {MODULES} from "@/constants/ielts"; +import {app} from "@/firebase"; +import {Module} from "@/interfaces"; +import {Stat, User} from "@/interfaces/user"; +import {sessionOptions} from "@/lib/session"; +import {calculateBandScore} from "@/utils/score"; +import {groupByModule, groupBySession} from "@/utils/stats"; +import {getAuth} from "firebase/auth"; +import {collection, doc, getDoc, getDocs, getFirestore, query, updateDoc, where} from "firebase/firestore"; +import {withIronSessionApiRoute} from "iron-session/next"; +import {groupBy} from "lodash"; +import {NextApiRequest, NextApiResponse} from "next"; + +const db = getFirestore(app); + +export default withIronSessionApiRoute(update, sessionOptions); + +async function update(req: NextApiRequest, res: NextApiResponse) { + if (req.session.user) { + const docUser = await getDoc(doc(db, "users", req.session.user.id)); + if (!docUser.exists()) { + res.status(401).json(undefined); + return; + } + + const q = query(collection(db, "stats"), where("user", "==", req.session.user.id)); + const stats = (await getDocs(q)).docs.map((doc) => ({ + id: doc.id, + ...(doc.data() as Stat), + })) as Stat[]; + + const groupedStats = groupBySession(stats); + const sessionLevels: {[key in Module]: {correct: number; total: number}}[] = Object.keys(groupedStats).map((key) => { + const sessionStats = groupedStats[key].map((stat) => ({module: stat.module, correct: stat.score.correct, total: stat.score.total})); + const sessionLevels = { + reading: { + correct: 0, + total: 0, + }, + listening: { + correct: 0, + total: 0, + }, + writing: { + correct: 0, + total: 0, + }, + speaking: { + correct: 0, + total: 0, + }, + }; + + MODULES.forEach((module: Module) => { + const moduleStats = sessionStats.filter((x) => x.module === module); + if (moduleStats.length === 0) return; + + const moduleScore = moduleStats.reduce( + (accumulator, current) => ({correct: accumulator.correct + current.correct, total: accumulator.total + current.total}), + {correct: 0, total: 0}, + ); + + sessionLevels[module] = moduleScore; + }); + + return sessionLevels; + }); + + const readingLevel = sessionLevels + .map((x) => x.reading) + .filter((x) => x.total > 0) + .reduce((acc, cur) => ({total: acc.total + cur.total, correct: acc.correct + cur.correct}), {total: 0, correct: 0}); + const listeningLevel = sessionLevels + .map((x) => x.listening) + .filter((x) => x.total > 0) + .reduce((acc, cur) => ({total: acc.total + cur.total, correct: acc.correct + cur.correct}), {total: 0, correct: 0}); + const writingLevel = sessionLevels + .map((x) => x.writing) + .filter((x) => x.total > 0) + .reduce((acc, cur) => ({total: acc.total + cur.total, correct: acc.correct + cur.correct}), {total: 0, correct: 0}); + const speakingLevel = sessionLevels + .map((x) => x.speaking) + .filter((x) => x.total > 0) + .reduce((acc, cur) => ({total: acc.total + cur.total, correct: acc.correct + cur.correct}), {total: 0, correct: 0}); + + const levels = { + reading: calculateBandScore(readingLevel.correct, readingLevel.total, "reading", req.session.user.focus), + listening: calculateBandScore(listeningLevel.correct, listeningLevel.total, "listening", req.session.user.focus), + writing: calculateBandScore(writingLevel.correct, writingLevel.total, "writing", req.session.user.focus), + speaking: calculateBandScore(speakingLevel.correct, speakingLevel.total, "speaking", req.session.user.focus), + }; + + const userDoc = doc(db, "users", req.session.user.id); + await updateDoc(userDoc, {levels}); + + res.status(200).json({ok: true}); + } else { + res.status(401).json(undefined); + } +} diff --git a/src/pages/exam.tsx b/src/pages/exam.tsx index 803a020a..52822cca 100644 --- a/src/pages/exam.tsx +++ b/src/pages/exam.tsx @@ -166,6 +166,8 @@ export default function Page() { }); } + axios.get("/api/stats/update"); + setUserSolutions([...userSolutions.filter((x) => !solutionIds.includes(x.exercise)), ...solutions]); setModuleIndex((prev) => prev + 1); }; diff --git a/src/pages/exercises.tsx b/src/pages/exercises.tsx index 4a21142c..8bb87b52 100644 --- a/src/pages/exercises.tsx +++ b/src/pages/exercises.tsx @@ -169,6 +169,8 @@ export default function Page() { }); } + axios.get("/api/stats/update"); + setUserSolutions([...userSolutions.filter((x) => !solutionIds.includes(x.exercise)), ...solutions]); setModuleIndex((prev) => prev + 1); }; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index f97e5dba..6dbcb916 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -16,6 +16,7 @@ import {Module} from "@/interfaces"; import ProgressBar from "@/components/Low/ProgressBar"; import Layout from "@/components/High/Layout"; import {calculateAverageLevel} from "@/utils/score"; +import axios from "axios"; export const getServerSideProps = withIronSessionSsr(({req, res}) => { const user = req.session.user; diff --git a/src/utils/stats.ts b/src/utils/stats.ts index 765b24a3..0ff7063f 100644 --- a/src/utils/stats.ts +++ b/src/utils/stats.ts @@ -116,6 +116,7 @@ export const getExamsBySession = (stats: Stat[], session: string) => { export const groupBySession = (stats: Stat[]) => groupBy(stats, "session"); export const groupByDate = (stats: Stat[]) => groupBy(stats, "date"); +export const groupByModule = (stats: Stat[]) => groupBy(stats, "module"); export const convertToUserSolutions = (stats: Stat[]): UserSolution[] => { return stats.map((stat) => ({