Binary Duel, front-end in 2 weeks with Svelte and DaisyUI
IAmNelu
Posted on March 10, 2024
Introduction
In the ever-evolving landscape of frontend development, developers like me are always looking for tools and frameworks that can help us build efficient, responsive, and visually appealing UIs. A powerful combination that is getting more and more attention is Svelte, a lightweight JavaScript framework for making interactive web pages, coupled with DaisyUI, a high-performance Tailwind CSS component library.
In this article, I will discuss this two-week journey to build a web app and learn this fantastic tooling combination.
What is the App?
The idea behind Binary Duel was to build a real-time quiz game for programmers. It was supposed to be easy to understand, snappy, and funny when possible. The game status can be divided into four main stages:
- Create/join a duel and wait for an opponent
- Choose a category
- Answer a question
- View the result of the game
The flow of a duel consists of these four stages in order, with a repetition of the two middle stages multiple times, depending on the number of questions per game.
The next step was then to choose how many pages to build and how they should interact.
Frontend Architecture
Unsurprisingly, the main dependency of the project is Svelte, and for that reason, I used SvelteKit to lay down the structure of the website. The wonderful thing about this metaframework is that it comes with filesystem-based routing: in short, means that depending on the folder structure of the project, the routing was already taken care of.
For passing data across the app, I leveraged the Svelte Context API instead. For state management, I used Tansu, a lightweight open-source library inspired by the implementation of Svelte stores.
The styling was handled by Tailwind and DaisyUI.
State Management
The state in the app relied on three main pillars: the Svelte Context API, the Tansu library, and browser Local Storage.
Context API
Since the game is divided into multiple stages with different pages, I needed a solution for having the same persistent state across the app. First of all, the state of the application is quite small, defined by individual Tansu stores that are accessible everywhere. The Svelte Context API works like a dictionary: using a key, you can store some values. Therefore, the dependency injection required for this was achieved in two steps: first, I stored the values, then, whenever I needed them, I could retrieve them. To make things easier, I used two helper functions, one for storing and one for retrieving. With this approach, I did not have to worry about which key to use.
An important notice: setContext
and getContext
functions must be called during the initialization of the component, exactly like the lifecycle methods.
const gameStatus$ = writable({} as GameStatus);
const gameStatusInjectionKey = Symbol('gameStatus');
function setGameStatus() {
setContext(gameStatusInjectionKey, gameStatus$);
}
export function getGameStatus(): Writable<GameStatus> {
return getContext(gameStatusInjectionKey);
}
In the Binary Duel web app, I decided to split the state into multiple sub-states. This granularity allowed me to use only the pieces of state I needed. I used a single function to initialize the state in the +layout.svelte
file at the top of the project, to be sure that no matter the component, I would always have access to the state.
<script>
import { initContext } from '$lib';
import '../app.css';
initContext();
</script>
<slot />
Tansu
The main reason why I used Tansu was that I wanted to experiment with a reactive library that was not framework-dependent and easily reusable for future projects. It implements all the features of the Svelte stores and can be used inside reactive blocks without any issues. It also comes with other features that were not leveraged during the project, but I will keep experimenting with it since it looks really promising.
Local Storage
One might ask why there is a need to use local storage if we are already using Dependency Injection and a store system (Tansu in this case). The answer is simple: I used the local storage as a fallback in case of a page refresh. As mentioned in the introduction, the idea was to build something lightweight and simple; therefore, I avoided sending more requests to the backend when possible.
Once again, I used the same implementation as for the Context API with getter and setter functions.
export function setGameStatusInStorage() {
localStorage.setItem('gameStatus', JSON.stringify(gameStatus$()));
}
export function getGameStatusFromStorage(): GameStatus | undefined {
const gameStatus = localStorage.getItem('gameStatus');
if (gameStatus) {
return JSON.parse(gameStatus);
}
}
export function clearGameStatusFromStorage() {
localStorage.removeItem('gameStatus');
}
Backend Communication
As a front-end developer, I only worked on the UI; the backend was done by a friend of mine. You can check his article here!
One might think that since it is a real-time quiz game, there is some sophisticated system to sync the state between the players...well, they would be wrong. This was a small side project for both of us, and we did not invest money into it (except for the domain name).
We were therefore limited by the free tiers, and the fastest and quickest solution that we developed was a polling system (more details about why we took that decision and did not go with sockets can be found here). In fact, when a player has made an action and is waiting for the opponent, their client will start polling the server with a request every second, demanding the status of the game.
Requests will be sent until the status changes, and the game moves on.
To safeguard the backend, we put limits in place regarding the maximum number of games and the number of requests that can be sent in a certain time period. These limit values were found after friendly DDoSing the backend multiple times.
Error Handling
I also implemented a basic error handling system in case of a communication error with the backend server or if trying to access a non-existent game. The app will redirect to the home screen, and thanks to the local storage, display the error message.
Customization
Tailwind CSS is the backbone used for all the styling present in the app, allowing me to write close to no CSS at all and using only helper classes.
For the components, I used DaisyUI with the customization theming tool present on their page. I could easily pick the custom color theme present in the app.
I also used tailwindcss-animated which made the animations on the screen in a few seconds.
The end screen drawings were my creations, of which I am very proud. They were first sketched and then redrawn with Inkscape to have them as SVGs. The same goes for the app's logo.
Hosting
For hosting, I went with Cloudflare's solution because the free tier is extremely convenient for what they offer and for the seamless integration with SvelteKit. In just a few minutes, it was possible to set up the process and automatically deploy the frontend for every update on the main branch (and they also offer the possibility to deploy previews for every PR on the main branch).
Another great feature of Cloudflare (they are not paying me; I just loved the service they offer) is the analytics they provide. In fact, they are not using any cookies and are really careful with data anonymization.
What Would I Do Better?
First of all, I want to make it clear that this was an experiment, and I do not consider myself an expert on the technologies used. I used them to become more familiar.
One thing that I would work on would be to better structure the app and extract more components to make it more modular and easier to maintain.
I would probably optimize the app a bit more to maximize Lighthouse metrics.
Even though I am really proud of the end-game screen, I would work on that to make it funnier and catchier, and maybe with more animations.
I would also experiment with some animations here and there throughout the game to make the app more engaging.
Regarding the game itself, it would be nice to have a global leaderboard and some ELO system, but that would require a login for each user and would go against the idea of a simple and easy-to-use app.
Maybe if there is demand, we will add more questions and more features to the app, so consider buying us a coffee.
Conclusion
We developed this app as a fun side project to experiment with technologies that we wanted to test, and I personally enjoyed working with my friend and creating this small, fun app. Soon I will have the code available so you can judge my mistakes 😇
Please consider reading my friend’s article. Please consider reading his article here.
Posted on March 10, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.