The burning question
Which team in the Canadian Premiere League has the best kit ?
A question asked each year when the new season’s kits are released, and the catalyst for many online debates. Before the 2024 season I thought it would be a neat project to create a site that would allow users to vote on which kits they thought were the best.
After very little research I decided on a head-to-head voting style for ranking because it’s much easier (usually) to pick which of 2 options you prefer but much more difficult, and time consuming, to rank a large number of items. I also find with a large number of items, after the top few best and bottom few worst the rest are just kinda - blah.
First up, collection
First things first, collecting the images. If I wanted to find out which kit ever released was the best I needed to get an image of every kit dating back to the year the league started in 2019, which was around 90 kits. With the help of some friends I was able to gather all the images I needed. I decided to store all the images in Cloudinary because they have a wonderful image component that made it so I didn’t have to worry about image sizes being sent to the client, it’s all handled for me by Cloudinary.
Once I had the images it was time to build out the database schema. I wanted to keep the database for this simple and store as little information as possible. I didn’t want to deal with accounts or user data (So no highest voted per user or voting history, thanks GDPR) which meant I was able to keep the database to 3 tables:
kit
kit_matchup
kit_vote
.
Creating the data schema
The kit
table is the most complex of the table with 3 data columns. The kit
table stores the team,
year, and version information for a kit that is used in combination to pinpoint a specific kit. Here’s
a few example rows:
id | team | year | version |
---|---|---|---|
69 | wanderers | 2023 | away |
84 | ottawa | 2024 | home |
24 | edmonton | 2019 | home |
The kit_matchup
table saves the combinations of kit matches that appear to users. This is a junction
table with 2 foreign key columns to the kit
table. It’s used to check how many total matches a kit
appeared in to when calculating the overall win rate. Because it’s used in calculating the win rate
rows are only created in this table when a vote has been cast. This prevents artificially tanking a
kit’s rating by refreshing the page whenever it appears.
id | kit_a | kit_b |
---|---|---|
4 | 69 | 84 |
The kit_vote
table is the simplest of the trio of tables, only needing to store a single id pointing
back to the kit
table to record the winning kit in a match. I add a new row to the table for every
vote cast.
id | kit_id |
---|---|
4 | 69 |
NOTE: All tables also include timestamps for when they were created, and one for when they are soft-deleted. I left them out of the tables for brevity.
Once I had the tables created I needed to fill the kit
table with all the names, years, and versions
for each kit. I would like to say I did this with a fancy seeding script so I could easily re-seed
the table if necessary, but I manually entered the data for each kit myself.
✨Artisanally crafted database seeding✨.
The final step of getting the database set up is creating the queries that will give me the data I need for the lists of “Top Voted” kits. That should be pretty simple, right?
Stored procedures in Supabase
In order to query the database from a frontend application I need to create stored procedures in Supabase that contain the queries. All of the stored procedures I created were variations on the example below:
create
or replace function top_10_kits () returns table (
kit_id bigint,
team text,
version text,
year text,
vote_count bigint,
matchup_count bigint,
vote_matchup_ratio float
) as $$
...long_af_query...
$$ language plpgsql;
This is the signature for the stored procedure with the sql query edited out to keep it short. The
procedure returns rows from the kit table with a couple new fields added on. The vote_count
, matchup_count
, and vote_matchup_ratio
are all calculated values I’m using to sort the kits.
The vote and matchup count are straightforward, they are just the sums of votes and match ups for a kit respectively. The match up ratio is where the main sorting ✨magic✨ happens. Well, not magic, just some simple maths really.
kv.vote_count::float / km.matchup_count as vote_matchup_ratio
It’s as simple as the number of times someone voted for a kit divided by the total number of match ups that kit has appeared in. This gives me a nice long number between 0 and 1 that I can manipulate to display as a percent. Speaking of displaying…
Now for the part the people see
I used Sveltekit because I just can’t get enough of Sveltekit right now. I find it really easy to work
with for how I like to build my web apps. I like the filesystem-based routing because I like being
able to see the path structure in my IDE. With Sveltekit there is also a separate +page.server.ts
file that will only run on the server. This allows for very clear separation of what is happening on
the server versus on the client.
I’m not a great designer (I’m working on it) so I tried to stick with just 2 colours. I used an eyedropper tool to find the two main colours of the Canadian Premiere League logo and went with those.
I like using a darker colour for the header/footer background so that naturally led me to choose the navy blue for that purpose and that left me with the obvious choice of green as the second colour. It was an obvious choice to me because I didn’t want to design with 2 shades of blue, that’s even too much blue for Eiffle 65. I decided to use the green for the page background, and not leave it relegated to accents, because it was reminiscent of a football pitch. It’s also less harsh on the eyes than a plain white background.
Voting/Landing page
For the main page I wanted to keep it simple, and didn’t get any last-minute brain waves, so I went with 2 cards that the user must choose between. Each card has a kit randomly chosen from the entire list of all kits, with a quick check to make sure the same kit isn’t used for both cards. Once the user makes their choice a toast message appears to confirm which kit was voted for and the kits are refreshed with 2 new choices.
Top Voted Kits
The “Top Voted Kits” page contains 4 versions of the “Top 3 X Kits” lists:
- overall
- home
- away
- third
The stored procedures query for the top 10 but I only end up displaying the top 3 because showing more would take up too much space on mobile (even on desktop in imo).
I added the numbered place to the top left corner of each card and styled it to look like a podium medal; gold, silver, or bronze. Each card also includes all the information about the kit and its match stats; how many votes it received, how many rounds it appeared in, and of course the vote ratio displayed as a percent.
Teams pages
Finally there is the Teams page and each individual page to see each team’s kits listed out. This way a user could check the votes, rounds, or ratio for any kit from any team regardless of its position in the overall standings.
I added these shortly after the site originally launched because people were asking me about how specific kits were doing. At first I would manually query the database to tell them but quickly thought that there had to be a better way.
I quickly built out a simple page of links listing the teams and linking to a page that would display the kits from that team. I did a “good enough” developer design for the content and never adjusted it, “good enough” never really had a need to get any better.
Plans For The Future
I will be adding the newly released kits for the 2025 season to my database but don’t know what else the future has in store for this site. I would like to update the design but don’t have any ideas to make it look better.
Sometime in the near future I would like to upgrade the site to Svelte5. I don’t think that should be a difficult update, it was pretty straight-forward when I did it for the site you’re reading this on right now.
The only other feature ideas that I had but never ended up getting around to were adding a “Lowest Voted” page and adding pages or filters to see the highest voted in a chosen time frame. Neither spoke to me enough to add them when I first created the app but maybe soon™️?