Quizzer
De Quizzer is een web applicatie die kan worden gebruikt in bars, sportkantines en misschien zelfs in gevangenissen om als team een pubquiz te spelen.
In de laatste opdracht van mijn cursus web development werd besloten om een pubquiz te bouwen die tijdens de vakantie met familie gespeeld kon worden. De student zou te maken krijgen met meerdere teams, rondes en vragen.
Het project is gebouwd met twee Single Page Applications, één voor de quizmaster en één voor de teams. De SPA's zijn gebouwd met React en om de quiz status bij de houden werd Redux gebruikt. AntD werd gebruikt als frontend-framework met Formik om alle formulieren af te handelen.
De backend is gebouwd met Express op Node.js en MongoDB met Mongoose. Omdat de UI-applicaties en de backend zo nauw met elkaar verbonden zijn, heb ik besloten om een monorepo te maken. De bonus van het hebben van een monorepo is dat ik componenten tussen applicaties kan delen.
Vanwege het monorepo-schema was validatie eenvoudig. Ik kon dezelfde schema's in de SPA's gebruiken als in de backend. Op deze manier kon ik er altijd zeker van zijn dat het juiste schema werd meegestuurd. Ik heb gebruik gemaakt van Formik
en dat had een geweldige implementatie metYup
. Iets dat ik in de toekomst opnieuw zou gebruiken.
// common/../quiz.ts
const QuizSchema: ObjectSchema<QuizSchema> = object({
name: string()
.min(4)
.required(),
language: string()
.oneOf(languages)
.notRequired(),
password: string()
.min(8)
.notRequired(),
}).defined();
Uitdagingen
The UI of the quiz masters and teams need to run in sync to make the pub quiz a pleasant expierence. To do this I used WebSockets to send a notification to the UI applications that there was a possible update ready. The UI then requested the new quiz state. Because the UI handles what is shown to the user, the backend does not have to deal with the UI status.
De UI van de quizmasters en teams moest synchroon lopen om van de pubquiz een plezierige ervaring te maken. Om dit te doen heb ik WebSockets gebruikt om een melding naar de UI-applicaties te sturen dat de quiz mogelijke geupdated was. De UI vroeg vervolgens om een nieuwe quiz.
Om de backend schaalbaar te houden heb ik ervoor gekozen om gebruik te maken van de MongoDB
change stream cursor. Wanneer er een update was in de quiz tabel werd er een event gestuurd naar de wachter. De backend stuurde dan op basis van het quiz ID een melding naar de verbonden UI's dat er een quiz update was.
// socket.ts
QuizModel.watch().on('change', change => {
if (change.operationType === 'update') {
const quizId = change.documentKey._id.toString()
sendUpdate(quizId)
if (change.updateDescription.updatedFields.isFinished) {
disconnectAllFromQuiz(quizId)
}
}
De quiz state is opgeslagen in een Redux store. Deze staat wordt bekeken door alle views en zij bepalen wat er getoond wordt. Mocht er een volgend project komen dan zou ik deze code graag refactoren om de leesbaarheid te vergroten.
// quiz.tsx
if (!quiz || quiz._id !== quizId) {
return <Spin />
} else if (quiz.isFinished) {
return <ScoreBoard quizId={quiz._id!} />
} else if (!quiz.rounds?.length && isApprovingTeams) {
return <ApproveTeamsComponent />
} else if (!quiz.rounds?.length && !isApprovingTeams) {
return <CreateRoundComponent />
} else if (quiz.rounds?.length) {
const round = quiz!.rounds![quiz!.rounds!.length - 1];
if (!round.questions?.length) {
return <AddQuestionRoundComponent />
} else if (round.questions.length === questionsLimit && round.questions[round.questions.length - 1].isFinished) {
return <CreateRoundComponent />
} else if (!round.questions[round.questions.length - 1].isFinished) {
return <ApproveAnswersComponent />
} else {
return <AddQuestionRoundComponent />
}