Generating a Waffle House Themed Receipt Using Spotify Data for Jonas Brothers
It’s the text message any hungry music marketer hopes to one day receive... “lee I swear there is a cool Waffle House activation just waiting to be launched around the Jonas Brothers lol” And it was sent to me by the amazing Sarah Gaupel from Republic Records. Yes, somehow the stars have aligned and the Jonas Brothers have treated us to a new song titled “Waffle House” from their forthcoming new record, The Album. How does one market a song called “Waffle House” without overtly creating a massive commercial for the similarly named 24/7 eatery? The answer lies in the spirit of the late night diner experience and the thematics of classic Americana style.
We kicked around many ideas but landed on a concept that was inspired by Melody Han You ingenious design project Album Receipts. Maybe it’s just me but I have a soft spot for music data being reinterpreted in familiar design constructs. I’ve seen albums as condoms, playing cards, pantone swatches, legos… hell, even I have created albums as floppy disks and sauce packets. However, there is something about “the receipt” which lends itself well to a fan loyalty activation and naturally, one inspired by houses of waffles.
When you visit our Spotify app, you will be paired with one of the three waiters (Nick, Kevin, or Joe) who work at the JONAS BROTHERS diner. Your waiter will take a look at how many songs you’ve consumed recently from the Jonas Brothers discography and calculate your total. He will then provide you with an itemized receipt (by album) of your consumption and recommend you return daily to pick up your receipt. Since Spotify data updates every 24 hours, you can really make a meal out of this thing.
Get your receipt today and read on to find out how this activation was served hot and fresh.
Spotify Integration
Similar to other Spotify apps I've developed in the past, we pulled the users top 50 recently streamed tracks from the Top Items endpoint. Once we had those tracks, we checked to see which of those were on one of the six Jonas Brothers records, including the upcoming one, The Album. Once we knew which of their top tracks were from Jonas Brothers albums, we could prep the data that would be required to visualize our receipt.
// Album tracks from a users top tracks
const albumTracks = () => {
// Filter top tracks
return topTracks.filter(track => {
// Return tracks which are off one of the albums
return jonasAlbums.map(album => album.id).flat().includes(track.album.id)
})
}
Receipt Data Prep
In order to prep our track data into something we could render on the receipt, we needed a good understanding of what the receipt itself should require. I was inspired by both Album Receipts and the actual Waffle House receipt to make each album a line item on the receipt which had a quantity of songs (from that album) and subtotal of duration (of the associated songs.) So we'll count the total songs from each album and then add up their millisecond durations as a subtotal.
// Build receipt data
const receiptItems = () => {
// Map all albums
return jonasAlbums.map(album => {
// Get tracks off album
let tracks = albumTracks.filter(track => album.ids.includes(track.album.id))
// Get track times
let trackTimes = tracks.map(track => track.duration_ms)
// Calculate sub total
let subTotal = tracks.length ? trackTimes.reduce((sum, x) => sum + x) : 0
// Return receipt item
return {
id: album.ids,
minutes: toMinutes(subTotal),
seconds: toSeconds(subTotal),
songs: albumTracks.length,
time: subTotal
}
})
}
You'll notice above that I've included the parsed minutes and seconds as part of this data also. This is more prep work for the visual piece as I knew I was going to show minutes as normal text but seconds as superscript. (Or at least I thought) Here's those helpers from my utils file.
// To minutes
export const toMinutes = (ms) => {
// Return minutes
return (Math.floor((ms / 1000 / 60))).toString()
}
// To seconds
export const toSeconds = (ms) => {
// Return padded seconds
return (Math.floor((ms / 1000) % 60)).toString().padStart(2, '0')
}
Now that we have the receipt line items, we can add up the subtotals to come to our overall receipt total. This number is very important because it's the number that fans will brag most about. (Who's run up the biggest total?) We'll also parse out the minutes and seconds here for later compositing.
const receiptTotal = () => {
// Return total duration
let total = receiptAlbums.map(album => album.time).reduce((sum, x) => sum + x)
// Return totals
return {
minutes: toMinutes(total),
seconds: toSeconds(total),
time: total
}
}
That's pretty much it for the receipt but we also had to prepare lines of dialogue for our waiters to speak when introducing a fan's receipt.
Waiter Dialogue

Early on during testing, I realized (or I was told) that users didn't really understand what the receipt meant. We were going to need to add more copy to explain what the song quantities and subtotals represented. Instead of adding another boring screen of copy, I proposed that we create a character dialogue you might see in a video game and the WaiterDialogue
component was born. After going through a similar process of preparing the track data into spoken lines of dialogue, we developed a component which could speak these lines, one at a time.
First, we setup some variables and computed properties. Of course, we needed the lines of dialogue the waiter would speak. We also needed to keep track of which line
we were on as well as which letter of that line we were currently displaying. The spokenLine
computed property is then simply the substring of the current line starting from the beginning and ending on the currentLetter
index.
// Initialize lines
const lines = waiterDialogue
// Line index
const currentLine = ref(0)
// Letter index
const currentLetter = ref(0)
// Current line
const line = computed(() => {
// Return current line
return lines[currentLine.value]
})
// Spoken line
const spokenLine = computed(() => {
// Return substring of current line
return line.value.substring(0, currentLetter.value)
})
Now, all we needed was a function which could increment through all of the lines and all of the letters. This can be achieved very simply with a setInterval
. Every 50ms, we'll increment the current letter. Once the current letter index is greater than the total characters in the current line, we check to see if another line exists. If so, we wait for 1.5s, increment to the next line, reset the current letter index to 0, and startSpeaking
again. If not, we can stop speaking and move onto the next action if our application. (Showing the receipt) Here's what that all looks like.
// Initialize speaking
let speaking
// Start speaking
// ----------
async function startSpeaking() {
// Start speaking interval
speaking = setInterval(async () => {
// Increment current letter
currentLetter.value += 1
// If current letter index is greater than total characters in line
if (currentLetter.value > line.value.length) {
// If next line exists
if (currentLine.value + 1 < lines.length) {
// Clear speaking interval
clearInterval(speaking)
// Wait a second
await wait(1500)
// Go to next line
currentLine.value += 1
// Start over from first letter
currentLetter.value = 0
// Speak the next line
startSpeaking()
} else {
// Clear speaking interval
clearInterval(speaking)
// Wait a second
await wait(1500)
// Emit complete
emits('complete')
}
}
}, 50)
}
Displaying this to the end user is as simple as outputting the spokenLine
computed property in your template.
<template>
<div class="waiter-dialogue">
<div>{{ spokenLine }}</div>
</div>
</template>
That's some of my favorite technical aspects of this build so let's turn our attention to the design.
Design
When this project was brought up to me, I was excited to see some clean design associated with The Album already. I see you Fututra. I was confident I could create my own spin on the existing album system while drawing on some inspiration from the classic Americana diner aesthetic. Of course, the first thing I mocked up was that Waffle House parody logo. Then, I headed into Figma to put together a design system.
System



I love simplicity in design. Understand the elements at your disposal and create a simple structure around them. For this project, I knew color and typography were key. After designing the "JONAS BROS" logo, I created the yellow color range palette and outlined the limited font heiarchy I might require. By making these dynamic in Figma, I could use them directly when turning my wireframes into screens. This system also made it a breeze when finally bringing my design to code with Tailwind.
Receipt

For the receipt itself, I was inspired by vintage diner receipts and in particular the dipped color technique used in the line items section. I paid close attention to small details like the width of table lines, the receipt #, and the "Thank you!" message. Then I could sprinkle on further easter eggs like randomizing your waiter name and table number. In the end, HTML Canvas was used to lay out all the dynamic copy we prepared earlier from Spotify. It was a lot of precise drawText
positioning which turned even more precise drawText
positioning when I realized I had to dynamically calculate the placement of all the minutes / seconds due to the fact that my "hand written" font choice did not have superscript characters for the seconds. All I'll say is, measureText came in handy. If you want to know more, just ask!
Dark Mode
Of course, my favorite easter egg is what happens when your device is in dark mode. The "JONAS BROS" diner sign lights up and begins to flicker. Tailwind has great dark mode support so this was pretty easy to pull off. I've gone ahead and recreated the technique on CodePen above. Just run to pen and click to toggle (or change your device mode.) I'm always into adding easter eggs to my work if there is time to do so.
Acknowledgements
Thanks again to Sarah Gaupel for inviting me to the “Waffle House” with her and Republic Records. I have no doubt this will be a massive hit for Jonas Brothers and spur many TikTok fueled visits to local diners. The song premiered on 4/7 and the Jonas Brothers performed it on SNL on 4/8. It’s such a huge month for waffles. 🧇