Lloyd Atkinson
Christmas Project 2022 - Music Quiz

Merry Christmas! Try out this fun Christmas music quiz I created!

Introduction

I continued the tradition I started for Christmas 2021 of creating a Christmas project I can share with family, friends, and possibly coworkers. Last year I made a Christmas trivia quiz using Vue. For 2022 I created another quiz but this time based on Christmas music, using Astro, React, Netlify (and Netlify serverless functions), and Azure table storage.

The player listens to a thirty-second song clip and is presented with four choices. I decided to have twenty songs. Fifty points are awarded for correct answers, and zero points for incorrect answers. With twenty songs, the total maximum score is one thousand.

Like last year’s project, the main goals for this were:

  • Be fun to play

  • Replayability - I want people to be able to play more than once

  • Easy to maintain and develop - I want whole parts or patterns I’ve implemented to be reusable for future versions and projects

  • Have two versions - a public-facing one and a family and friends version to be able to show some photos and memories from our year (me and my partner)

A new goal for this version of the project was to introduce a scoreboard. This year’s project has been very well received, just like the previous project, but the scoreboard has been a focal point; it’s been great fun seeing people try to beat either their score or a relative’s score!

I originally intended the scoreboard to be a “nice to have” if time permitted, and I’m glad I was able to add it. It will definitely be a mainstay feature from now on.

Overall Architecture

The application features a scoreboard, score submission, and the quiz feature itself. I am very familiar with Azure, so using Azure Table Storage was easy. While I could have hosted the site on Azure, Netlify is free below a certain usage. The Netlify functions (which are running on AWS Lambda) contain the business logic for score validation, persisting scores, and reading scores.

I created these diagrams using D2, a new diagram language. I have not used it until now, but it is potentially a significant improvement over existing tools. It’s also fairly new, which is probably why the second diagram feels wider than is ideal.

Architecture Diagram

Styling

This year’s project looks very similar to the previous project. I like this, as it adds a sense of identity. The same colours, fonts, aesthetics, and UX/UI principles were applied. I use Tailwind for styling, some CSS for the animated lights, and the tsparticles library for the snow (though one of the libraries I’ve been experimenting with this year, P5, has given me plenty of ideas as to how to implement particle systems - a topic for another time).

React/Frontend

Last year I wrote the project with TypeScript and Vue. This year I wrote the project with TypeScript and React. This is the frontend setup I will be using for future Christmas projects. Now, as two different UI frameworks were used, I couldn’t reuse the components, but I could reuse the TypeScript. But, as it turned out, I only used a little of it and created it mostly from scratch using the code from the previous project as a reference.

  • Code in both versions is generic, mainly with a good amount of reusability

  • In both projects, I made good use of domain modelling with TypeScript to ensure good quality and maintainable code base

  • The core of the 2021 project is a “renderless” Vue component

  • The core of the 2022 project is a React reducer

  • Both patterns are ways of abstracting/extracting logic into a reusable module

  • Creating a quiz-orientated mini-framework was something I’ve now only done twice - there is more domain knowledge involved in creating good quality, testable, and reusable quiz/questionnaire code than is perhaps immediately obvious

Now that I have done this twice, I am happy to proceed in creating a fully reusable component/set of components for future quiz projects. After implementing it twice with different but overlapping approaches, I’m confident in getting it right. It can be problematic to introduce abstraction too early in the design process. I’m glad I used React this time around.

Frontend

The diagram covers the overall structure of the code with the reducer at the core of the application. A nice pattern I’ve noticed with any kind of explicit state update pattern is how they are all similar and start to coalesce into the same concept. For example the TypeScript discriminated union I’m using to define the reducer. These are the only possible actions that can be dispatched. It’s not totally unlike finite state machines which I’ve written about before, the command pattern, and MVU (model-view-update). This is a whole topic by itself and I’m excited to write more about it in the future.

type Action =
| { type: 'start' }
| { type: 'answer', payload: { answer: ChosenTrack } }
| { type: 'finish' }
| { type: 'reset' };

I mentioned creating domain models (as any code base should have anyway), so here are some of them. The TrackInfo and Track are used across the code application, while the TrackWithPossibleAnswers is used exclusively in the reducer state to track given answers and display to the player which songs they answered correctly at the end.

export type TrackInfo = Readonly<{
artist: string;
name: string;
}>;
export type Track = Readonly<{
info: TrackInfo;
audioUrl: string;
imageUrl: string;
}>;
export type TrackWithPossibleAnswers = {
track: Track;
wrongAnswers: readonly TrackInfo[];
};

Animation

In the 2021 project, I only had small amounts of animation when transitioning between questions. I could have used the same, but I have been experimenting with an excellent library for React called Framer Motion. It describes itself as enabling “Production-ready declarative animations” with a strong focus on creating state-based animations. I’ve only used some of its features for this project, but I intend to use them more. For example, spring-based animations are used when the user interacts with the button to give it a random wobble.

Netlify and Netlify Functions

As I previously mentioned, for this project I implemented the scoreboard functionality using Netlify functions. For this project it made sense to use them as-is, but in future versions of these projects I’d like to design a more thorough architecture in Azure. The Netlify functions in that scenario would then simply be using a library I create that calls the Azure functions.

In it’s current form, I use the Azure table client library like this:

const fetchScores = async () => {
const client = getTableClient();
const partitionKey= process.env.STORAGE_TABLE_PARTITION!;
const results = await client.listEntities<ScoreBoardEntry>({
queryOptions: {
filter: `PartitionKey eq '${partitionKey}'`,
},
});
let scores: ScoreBoardEntry[] = [];
for await (const entity of results) {
const { name, score, date, location } = entity;
scores.push({ name, score, date, location });
}
return scores;
};

Azure Table Storage

There are many (check the sidebar for all the related pages!) reference table designs and recommendations for designing solutions with Azure Table Storage. In this case, the choice of partition key was clear. I have two deployments of the quiz site - the public-facing one and the private one for family and friends. Essentially the table is multi-tenant. The row key is a string concatenation of the user’s name, the current date, and some random hexadecimal characters. A more optimised table design would be possible too.

Validation, schemas, and Zod

I added some basic validation to the API as I knew that people would likely try calling the API with invented scores for the public instance (and I did see people trying it via logs). I used Zod, which is a very nice TypeScript library with a huge feature - type inference from a schema.

This means we get back an actual TypeScript type by writing a Zod schema! The crucial part of the following code is the z.infer<typeof schema>. I then follow my usual practices of marking most types as immutable.

import { z } from 'zod';
export type Meta = Readonly<{
ipAddress: string;
location: string;
userAgent: string;
date: string;
}>;
export const schema = z.object({
name: z.string().min(1).max(30),
score: z.number().min(0).max(1000),
});
export type Score = Readonly<z.infer<typeof schema>>;
export type ScoreEntity = Readonly<Score & Meta>;
export type ScoreBoardEntry = Readonly<Score & {
date: string;
location: string;
}>;

Conclusion

I had fun creating this year’s project iteration, and it was rewarding to see real people not only use something I designed and wrote but also enjoy using it! As a result, I already have some ideas for next year’s project.

  • The next iteration of the scoreboard needs to implement a more robust response to people calling it directly and/or trying to spam submit incorrect API requests. I don’t intend to create a user account system or any authentication for these projects, as they are supposed to be fun and easy to play. Even then, there are several features I will implement next time that can eliminate a significant amount of bad requests.

  • I am super happy I wrote this version with React and TypeScript

  • Now that I’ve created a similar project twice, I am in more of a position to make a usefully generic system (now with a scoreboard!)

  • Seeing people enjoy something I’ve created is not something I ever experience in my current job, so that was a very welcome change

Merry Christmas!

Share:

Spotted a typo or want to leave a comment? Send feedback

Stay up to date

Subscribe to my newsletter to stay up to date on my articles and projects

© Lloyd Atkinson 2024 ✌

I'm available for work 💡