GAME LEADERBOARDS PT. 1 KIND OF MAKING A WEBHOOK
it's like co-op, but i don't get paid!
    hellooooooooooo
my first co-op was at a startup called Swapt, which helps companies improve offline customer retention through engaging and securely routed QR codes. my main thing was building out client-customizable games, which eventually became a huge part of the company's product. pretty cool! one funny thing about it was that i ended up building an entire SQL-based leaderboard system that was scaled to the entire Boston Calling music festival before i even took database design and learned what the difference between a left and right join are. northeastern is one crazy school! 

regardless, i've been making games on my personal website (this one! archizzl dot com baby!) since my freshman year of high school, and i've always wanted to add automated leaderboards to them. kids in my classes would play the games, get a high score, and clamor at my gates for some glorious totem to their accomplishment, an eternal monument to their superior sportsmanship etched in obelisk to last til the next epoch or maybe until Juan beats the score four minutes later, to be dethroned by Ruby soon after. And on and on. 

i initially acquiesed to their demands, or rather, said "okay", and then thought to myself, how the fuck do i go about this? i didn't know anything about database management or backend development, i was busy with APUSH DBQ prep. so, my grand solution. . . .

hard-code each new high score into the page whenever a kid would approach me in person and ask me to
that's right! maybe the single least efficient thing i could have possibly done. i do not think there is a less technical way of implementing a leaderboard for the online game. maybe putting my phone number up on the page and instructing constitutents to ring my line for the most recent list of big-shot scorers. ridiculous. today, i'd like to right those wrongs, or at least one of them. starting small, as always. let's try adding a leaderboard to this game using csvbase, a free platform for hosting csvs online! let's start by making our table. while a long term plan for the site is to let people log into the site (which i'll be using my one free private csv provided by csvbase for), for now, we'll forego consistent clients for efficiency and just have people enter their name if they beat the high score of the game. since i made this game in high school, some of the code makes absolutely no sense. that's alright though, it's modular. maybe. here's the csv.
i added in a sample row. i couldn't help myself!
there's a link to an api page that gives us instructions for reading and writing to our page. let's try grabbing our api key (not telling!) and putting this leaderboard together!
async function getLeaderboard() { try { console.log("fetching!") const response = await fetch('https://csvbase.com/archizzl/archizzl-games-v1'); const text = await response.text(); console.log(text) } catch (error) { console.log("fetch failed") console.log(error) } } getLeaderboard();
fetching! click_speed.html:107 csvbase_row_id,name,game,score 1,archie,click_speed,5
great! we can read to our table! how about writing to it? csvbase uses Basic auth for authenticating requests to its API. well, it doesn't really have an api. it's a whole thing. ignoring the minutiae of everyday life, what this means is that csvbase requires you to authenticate if you want to write or delete from a table, and using basic auth means that the api auth for one of my tables is the api auth for all of my tables. no problem, i thought to myself, i'll just use an environment variable to hide my token. well, no. you can't. if there was an individual api key just for this table, i would say screw it, i don't care much about people screwing around with this leaderboard, i just want to try implementing it. but i can't risk someone having the key for my whole account! i only have one account! we'll have to hide it. but how? i'm trying to get this done for cheap. i don't want to go to the effort of hosting a server or pay for lambda or whatever. how about. . . my favorite hacky solution for everything. . . GOOGLE APPS SCRIPT! update: wow! that was painful! but i got it to work! here's our code in a new (free) Google Apps Script:
function doPost(e) { var data = JSON.parse(e.postData.contents); payload = JSON.stringify(data) var options = { "method" : "POST", "contentType" : "application/json", "payload": payload, "headers": { "Authorization": "Basic XXXXXXXX=", "Content-Type": "application/json" }, "muteHttpExceptions": true }; var url = "https://csvbase.com/archizzl/archizzl-games-v1/rows/" var response = UrlFetchApp.fetch(url, options); return ContentService.createTextOutput(response); }
i've obfuscated my API token. it would be funny if i didn't, and this whole thing was moot. Regardless, this is a webhook! I can now make post requests to my deployed web app Apps Script with my player name and score, and then it will be updated to the csv! awesome!
note: anyone can make post requests to this web app. uh oh! that means it isn't secure at all, it's quite vulnerable to trolling and etc. however, the vulnerability is localized to just this leaderboard. the endpoint is open and exposed, but my account is not.
now, we just add in the front-end section.
or so i thought. . .
about a half an hour later, strangely enough, i cannot for the life of me seem to get my new web app to accept POST requests sent in from my local server. it's still working perfectly in postman, but not in the app. what on earth is going on?
fetching! click_speed.html:116 { "error": "unable to convert the data you provided to the required type" }
that's an error code from csvbase, meaning that our request is getting to csvbase, but isn't going through. what's changed between the fetch and postman? to be completely honest, i'm stumped right now. i tried troubleshooting through countless stackoverflow threads, using code snippet generation from both Postman and ChatGPT, and stared at it for a long time. now i'm hungry, and tired, and i've accomplished something (setting up a webhook!). i think i'm going to quit while i'm ahead. see you next time!
SEPARATING BLOG ENTRIES INTO TEXT FILES, SOME MARKDOWN STUFF
i'll just meet you there!
    Hi! 
As of right now, I manage two pages that have a blog-like structure to them. This one, and the website for my band Sweetums. currently, i write out the entries as separated html divs straight into the source code of each 'blog'. it works fine, but it can get a little frustrating sometimes trying to scroll around and find stuff, or really get into writing something without being distracted by the code flying around. today, i'm going to try to take a page that looks like this:

to instead be empty and then populated with a script sourcing each song from a folder of text files. i'll try doing that for the sweetums website first, and then depending how smoothly that goes, i'll do it for this blog too!

i'll start by creating a new folder at the root directory of the sweetums repository and just call it "songs", and set it up to represent one of my favorite songs that i dearly wish i've written, Roadrunner by The Modern Lovers.

before we set up our text file, here's an example of a song file and how it's formatted:

Each song has a title, lyrics, images, tabs, and notes. You might notice that the chords are formatted with brackets -- deep at the bottom of the source code for the sweetums site there's a script to process this fake little markdown language, taking any characters that are found between a [ ] and making them bold and with a larger font. this way, when i write out the songs, i don't have to go to the trouble of separating the chords into new divs or spans every time. 

for example, we can take this text (inside an html div)
[C] [G] well the lease is up, and i don't know the terms of the legalese but i'm pretty [F] sure i'd have to [WHY DIDN'T YOU SIGN IT?] (have someone look it over first) i could've. i could've. [C] [G] anneliese is up, or at least i hope she is, [F] becuase she just sent me a picture of [SHE'S STARING AT HER WALL] (her cat against a yellow wall) i should be with her. i miss her.
and it'll display like this: what we're going to do with this text file conversion is sort of one step beyond. we're going to set up the text file with text delimiters. . .
(e.g characters that can be recognized by a script as indicating something related to how we should be organizing our data rather than something for the user to read. for example, in a csv (comma separated values) table, columns of data are separated by a comma. so, you can represent any amount of data in a simple, straightforward way. let's say you're a dj keeping track of songs people requested at a set on the fly:
date, venue, person, song 09/21, tourist trap, sophia, heart of glass (bossa nova remix) 11/22, sinclair, cameron, cosmic surfin
can then be uploaded into any spreadsheet software or parsed by a script, easy as pie.) not all csvs have a comma delimiter, but that's another can of worms.)
. . .to automatically process our sections into generated divs, and then from there, process the text in the divs to whatever specific text markdown they need! i know i could probably use an already existing markup language to speed this up, but where's the fun in that? here's a first pass-through. i'm using a double @ sign as a delimiter. in order, we've got our song title, the lyrics, images, tabs, any special notes on it! it's music notes, not music notes. it's a good idea to choose something you would never use in any context but as a delimiter. now, let's load our txt file as a string in javascript and split it up into its separate elements. here's my first attempt at grabbing our roadrunner.txt file:
async function fetchSong() { try { console.log("fetching!") const response = await fetch('songs/roadrunner.txt'); const text = await response.text(); console.log("logging roadrunner") console.log(text) } catch (error) { console.log("fetch failed") console.log(error) } } fetchTextFile();
does anyone want to guess what happened? some mysterious reader much more well-versed in web development than i? or maybe just someone with common sense? here, i'll hide the answer. highlight the text right after this to see: Access to fetch at 'file:///Users/archieoconnell/dev/sweetum.github.io/songs/roadrunner.txt' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, isolated-app, chrome-extension, chrome, https, chrome-untrusted. what's going on here?
you might recognize the evil mosquito that is CORS, which you can read more about here. long story short, CORS makes the internet "safer" and "more secure" blah blah blah, e.g it's a pain in the ass to access resources from one webpage to another
we're trying to load the .txt file from a local file:// path, which is not how the fetch api works. it has to be one of the whitelisted schemes (https, data, etc.). when i try grabbing the content of a text file that's up on the web already, like, say, flagday.txt (more on flag day later), our scripe works PERFECTLY! how can we use a whitelisted scheme while still working locally with our roadrunner.txt that hasn't yet been pushed to github? we'll start a local server with python!
➜ archizzl.github.io git:(master) ✗ python3 -m http.server Serving HTTP on :: port 8000 (http://[::]:8000/) ... ::1 - - [21/Jul/2024 17:20:16] "GET / HTTP/1.1" 200 - ::1 - - [21/Jul/2024 17:20:16] "GET /hpi/salmon.mov HTTP/1.1" 200 - ::1 - - [21/Jul/2024 17:20:16] "GET /hpi/indexbackground.mp4 HTTP/1.1" 200 - ::1 - - [21/Jul/2024 17:20:16] code 404, message File not found ::1 - - [21/Jul/2024 17:20:16] "GET /favicon.ico HTTP/1.1" 404 - ::1 - - [21/Jul/2024 17:20:25] "GET /log.txt HTTP/1.1" 200 - ::1 - - [21/Jul/2024 17:20:32] "GET /b.txt HTTP/1.1" 200 - ::1 - - [21/Jul/2024 17:20:35] "GET /blag.html HTTP/1.1" 200 -
by opening a terminal from the directory we're working in and using the python command python3 -m http.server, we can now access the page we're working in from the port localhost:8000! you'll also get to see an ongoing log of any requests that you make to your local server! you can close it using control + c
(or at least if you're on a mac. i'm not sure about other platforms)
now, when we try it after opening a server in the sweetum repository, we get:
fetching!
(index):1860 @@ Roadrunner @@ One, two, three, four, five, six Roadrunner, roadrunner Going faster miles an hour Gonna drive past the Stop 'n' Shop With the radio on I'm in love with Massachusetts And the neon when it's cold outside And the highway when it's late at night Got the radio on I'm like the roadrunner Alright @@ https://images.squarespace-cdn.com/content/v1/600594e0259ef06ca93df13f/1611700204786-C9KJL0LTFR154X6VVM6K/TT-60547-Richman+10162.jpg @@ e|---------------------------------------------------------------| B|---------------------------------------------------------------| G|---------------------------------------------------------------| D|---------------------------------------------------------------| A|---------------------------------------------------------------| E|---------------------------------------------------------------| @@ this song rocks!
porpoise.dashboard.wrbbradio.org/:1 Access to XMLHttpRequest at 'https://api.wrbbradio.org/getSchedule' from origin 'https://porpoise.dashboard.wrbbradio.org' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
we're still getting a CORS issue, but that is for a different, stupid reason that i will get to at another point. perhaps i will write about it and link back to it here. for now, all you need to know is that you don't need to know about it. we've got our song! let's load it in!
(at this point, i decided to switch to doing this with an actual song by my band so that whatever i'm doing can be troubleshot with a real example. sorry to all the modern lovers out there! peace and love!)
here's some more code for you:
songs = ["pinky"] for (song of songs) { fetchSong(song).then((foo) => { console.log("foo:") console.log(foo) console.log(foo.split("@@")) }) }
since we're just working on song right now but want to eventually have this work for every song, we're going to define our songs as an array and then use a for each loop to run our function for each song we want to display on the sweetums website. you might have noticed above that our fetchSong() function had an async label. async functions return Promises, which are then accessed using the .then() property. We need to use an async function because we're using the fetch api to request resources from a web server, so our code should (a)wait a response before proceeding with its operations. our response looks something like this:
(5) ['Pinky%%pinky%%https://maps.google.com\n', "\n \n [G] [B7]…t at least it's not a spiderbee\n like me!\n", '\nhttps://upload.wikimedia.org/wikipedia/commons/1/…GcSlknVy6nECWqgdwVY78ysRMlFZWXT3V89CJl2QhRMoZQ&s\n', '\n NO TABS! \n BUMMER!\n', '\nat "take your breath away", drum beat switches to… *\nand strums go slower. father john misty vibes'] 0 : "Pinky%%pinky%%https://maps.google.com\n" 1 : "\n \n [G] [B7] \n (oh,) won't you fix me, fix me\n [C] [G]\n will you heal my broke-back pinky\n (Amaj7) [D]\n when my hands finally do what i want them to do\n [G] [B7] [C] [Cmin]\n and snap themselves in half\n [G] [Emaj7] [Amaj7] [D] [Amaj7] [D] [G]\n so they can never put their grip around your gentle neck again\n\n [G] [B7] \n (oh,) if you'll only wish me\n [C] [G]\n to give up what i brought with me\n (Amaj7) [D]\n i'll wrap it up and spit it out and chew it up and throw it out\n (aughhghgh!!!)\n [G] [B7] [C] [Cmin]\n so dear, if you'll forgive me, we'll walk around sing tweedle-dee\n [G] [Emaj7] [Amaj7] [D] [Amaj7] [D] [G]\n i'll be your one and only spiderbee!\n\n ++\n\n same chords for two solos\n\n ++ \n\n (just keyboard)\n\n [G] [B7] \n i can be better, folded down\n [C] [G]\n an upright pillar, a drone too loud\n (Amaj7) [D]\n while baby midas combs around the wreckage\n [G] [B7] [C] [Cmin]\n he doesn't catch those sordid eyes linger on what he leaves behind\n\n (hold Cmin)\n\n [G] [Emaj7] [Amaj7] [D] [Amaj7] [D] [G]\n it makes you wonder why we pay so much for that shit anyway!\n\n [G] [B7] \n (oh,) won't you fix me, fix me\n [C] [G]\n will you heal my broke-back pinky\n (Amaj7) [D]\n when my hands finally do what i want them to do\n\n ++ \n\n [G] [B7] [C] \n and take your breath away\n [G] [B7] [C] \n leave my claw marks there to stay\n\n i'll be your\n [G]\n widowmaker\n [B7]\n endless taker\n [C] \n apologetic motion faker\n [Cmin]\n former governor charlie baker\n artisinal sourdough creator\n point guard for the l.a lakers\n\n [G] [Emaj7] [Amaj7] [D] [Amaj7] [D]\n baby, i can be a spiderbee, it's a real thing!\n\n [G] [B7] \n (oh,) won't you fix me, fix me\n [C] [G]\n will you heal my broke-back pinky\n (Amaj7) [D]\n when my hands finally do what i want them to do\n \n and \n get a little brittle\n when i rosin up the fiddle\n on a wednesday night\n with my whereabouts a riddle\n where'd he get ya?\n in the middle!\n does it sting?\n just a little!\n but at least it's not a spiderbee\n like me!\n" 2 : "\nhttps://upload.wikimedia.org/wikipedia/commons/1/1b/Spiderbee.jpg%%spiderbee\nhttps://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSlknVy6nECWqgdwVY78ysRMlFZWXT3V89CJl2QhRMoZQ&s\n" 3 : "\n NO TABS! \n BUMMER!\n" 4 : "\nat \"take your breath away\", drum beat switches to\nride * * * * * * * *\nsnare * * *\nkick * * * *\nand strums go slower. father john misty vibes" length : 5 [[Prototype]] : Array(0)
which is a bit scary! but it's working!! our .txt file has been grabbed and split up by its delimiter into an array of its sections. now, we can use it to populate our songs. right? well, while putting together the .txt file for pinky, i ran into a slight problem -- the photo i use for the song entry for pinky has a strange size formatting issue. to amend it, i gave it a somewhat hacky solution, which was giving the image a unique id and formatting it directly from the css of the page, making it an outlier from the other images. another issue is that i forgot each song has an id and an optional link, along with everything else. we're starting to build a system with a lot more complexity than we initially prepared for. have we been too cocky? let's take a step back and reassess what the point of this project is. the lyrics of my songs take up too much space in my html page, and i would prefer writing my song lyrics unencumbered by code flying around. that's all. it's never been a problem managing the images or titles or ids. let's scale our attempts back. new goal: just write our lyrics in the text file. manage everything else in the html. maybe we'll add tabs and notes at a later time, but right now, we're just focusing on getting something done. here's the code:
async function fetchSong(song) { try { const response = await fetch('songs/' + song.getAttribute("id") + '.txt'); const text = await response.text() return { "foo": song, "bar": text } } catch (error) { console.log("fetch failed") console.log(error) } } songs = document.querySelectorAll(".song") songs_navbar = document.getElementById("next_top_link") show_songs = [] for(song of songs) { fetchSong(song).then((love) => { song = love["foo"] song.querySelector(".brochure").querySelector(".lyrics").innerHTML = formatText(love["bar"]) show_songs.push(song) foo = document.createElement("a") foo.setAttribute("href", "#" + song.getAttribute("id")) foo.innerHTML = song.querySelector(".song_title").querySelector(".song_link").innerHTML songs_navbar.appendChild(foo) }) }
now that we've got this working, it would be pretty straightforward to set up the template with tabs and notes too, using a handy dandy delimiter and split() method. but i'm not going to do that right now. because i have other stuff to do! but thanks to my halfway goal adjustment, something was finished instead of a bunch being half-assed :)
MAKING THIS BLOG
it was a blast!
    hello world! 

    as a fun little experiment, for this first blog post, rafhsjkdafhsdljkafhklajdshfjkldsahjftestlongstringforoverflow i'm going to write out the steps i take to build this blog and keep a lot of the minutiae that i use to test it, while i'm making this blog! there's a lot of troubleshooting to be done, and a lot of design decisions to be made. right now, i'm trying to make it so the pre div we're working in for each blog entry automatically wraps its lines so they can all fit on the page.

    

    i'm going to try using the following commands i got from this thread:

    

    this gets us. .

    

    this kind of fixes it, but now everything's a little weird. let's try wrapping our lines in vscode and see what happens.

    

    now it looks okay, but because of the by-eye freehand indenting i was trying before, our lines still look wonky. let's try cleaning this up.

    

    notice that although the lines wrap, the numbers on the left side only change when you purposefully indent! this gets us. . .

    

    there we go! our fixes were enabling pre-word break wrap white space for our pre entries, and then turning on text wrap in vscode and not trying to do our line breaks by eye.
    also, since i'm writing this blog entry as i'm operating on the text within this blog entry. everything looks different all the time, and we're playing a little bit of an inception game. what's different here?

    well, while i was writing up those previous entries, i noticed that it became hard to discern what was an image of the blog text and what was the actual blog text.

    

    like in this example. what am i looking at?? what's an image, what's text? you can kind of tell, because of my poor cropping skills, but not really. here's the fix:

    
    

    hmm. . . this looks good, but i kind of can't tell which images are which, now. they're a bit squished together, like in this example:

    

    let's just add a 5 pixel margin and 2px padding on each image.

    

    which should get us something that looks like this:

    

    which has a lot more comfortable spacing. by the way, did you know that you can select hex codes for colors through VSCode? like this!

    
    i found that incredibly convenient.

    while typing that, i realized that it's hard to see the images because some of them are quite small! so i added in JUST TWO LINES! to make the images get big and visible when you hover over them. most solutions use javascript and have triggers and onclicks and are maybe just a little bit more fancy but no no no!! just two sweet lines! one more for vanity!

    
    

    ok. although that was a fun setup, in reality, it glitches out pretty hard and doesn't work at all on mobile, so we're just going to have to bite the bullet and make a modal with some simple javascript. it won't be too bad. 
in another life, i could be more like aem1k and figure out some brilliant css code golf solution, but i'm not there yet. YET!
there isn't really an official defined definition of what a modal box is in terms of web design, but loosely, it's a popup! like when you open a website and it asks you to check or uncheck cookie permissions before proceeding. that's a modal. in our case, our modal is something that the end user of the blog actually cares about and wants to see, but that's besides the point. usually, modals are dismissed by clicking on some X button in their corner, or clicking somewhere on the screen that is not on the modal. let's implement both, just because we might as well.
window.onload = function() { entries = document.querySelectorAll(".entry") for (let entry of entries) { classes = entry.getAttribute("class").split(" ") id = entry.getAttribute("id") new_link = document.createElement("a") new_link.setAttribute("href", "#" + id) new_link.innerHTML = id links.appendChild(new_link) } }
this is all the javascript we currently have on the page. it grabs every article title and generates a list of links to each blog article, using their id, which is the datetime (ish) that the article was started. i put the datetimes in manually, but i'm sure there's some code whiz way of not having to, using the time of the first git commit of an article or something. i'll add that to the to-do list!
btw: new development: code blocks. i realized it was probably innefficient in pretty much every way to be including individual screenshots of the code i'm writing. more and more trial and error! by the way, you might have noticed that this text is small! and a lighter color! in pursuit of a consistent styling to this blog, i've created a series of tags to format my entries with. right now, there's
.blog_headers { font-size: 30px; color: red; } .blog_subheaders { } .blog_mini { opacity: 0.7; font-size: 10px; }
this text (yes, this! what you're reading with your eyes right now!) is a blog_mini. i haven't written any rules for blog_subheaders yet, which vscode is not thrilled about, but i will at the end when i write a witty subheading for this entry.
the trained eye might have noticed that these posts are all written in pre tags. i like them a lot. pre tags define formatted text, meaning anything written inside them preserves line spaces and tabs and spaces. so i can do this ! or _,.---.---.---.--.._ _.-' `--.`---.`---'-. _,`--.._ /`--._ .'. `. `,`-.`-._\ || \ `.`---.__`__..-`. ,'`-._/ _ ,`\ `-._\ \ `. `_.-`-._,``-. ,` `-_ \/ `-.`--.\ _\_.-'\__.-`-.`-._`. (_.o> ,--. `._/'--.-`,--` \_.-' \`-._ \ `---' `._ `---._/__,----` `-. `-\ /_, , _..-' `-._\ \_, \/ ._( \_, \/ ._\ `._,\/ ._\ `._// ./`-._ THIS! `-._-_-_.-' every character in a pre has the same length, so ascii art will always format correctly. what's interesting, though, is that you can put html tags inside them. e,g, your divs and as (links) and bs (bold) and is (italics) and they'll all work. yet, the characters that you use to write out the tags aren't actually factored into the formatted text. for example, in our wonderful turtle illustration above, the code looks like this: but the turtle looks a-okay! anyway, back to the javascript. we're going to create a function called modal that triggers the on and off of a modal popup and sets the popup to display whatever image we just clicked on. if you click outside of it, it'll pop back out. the code looks like this: html:
at some point i will go through these and write a markup script to let me display html tags as plain text. today is not that day. it's a div id=image_modal with a child div id=modal_image. image_modal has an attribute of onclick=modal()
css:
#image_modal { height: 100vh; width: 100vw; display: none; justify-content: center; align-items: center; flex-flow: column; background-color: rgba(0,0,0,0.5); position: fixed; top: 0; left: 0; z-index: 5; } #modal_image { height: 80vh; width: 80vw; background-size: contain; background-repeat: no-repeat; } #image_modal:hover:not(:has(*:hover)) { cursor:pointer; }
javascript:
images = document.querySelectorAll("img") for (let image of images) { image.onclick = function() { modal(image) } } popup = false; const modal = (image) => { if(image) { image_modal.style.display = "flex"; modal_image.style.backgroundImage = "url('" + image.getAttribute("src") + "')" } else { image_modal.style.display = "none"; } }
go for it! check it out! let me know if it works okay! one more thing that i noticed that is a little weird is that it flashes white at the bottom while i scroll, but that's probably an image loading thing. i'll figure it out. until next time.
check out the bouncy subheader :)