Quoteshelf
In case you couldn’t tell, I enjoy reading a lot. I also like to record my experiences, for example, by tracking the books I read on The Storygraph, and tracking the movies I watch on Letterboxd.
There’s an app called Readwise which is great for readers like myself. In the app, you can point your phone’s camera at the text on a page, and it will use OCR to save it as a quote. The app also allows you to review the quotes that you’ve saved in the past. It’s fun to revisit the favourite bits from books that I’ve read. The Readwise app implemented well, and I found it useful enough to pay for a subscription.
Having said that, I’m a firm believer in owning one’s data, so I decided to try to create my own solution. Introducing… “Quoteshelf”! This new section of the website contains all of the quotes that I’ve exported from Readwise. On the main page, I can swipe through a random selection of quotes, and I can browse the author index to find specific books.
My fourth-year ray tracer project
Even back then, I loved pool
The Shuffler
There was an idea…
I was building the 404 page for this site and a random idea occurred to me. I know that from a 404 message, you should always link back a valid page, usually the home page, but then I thought it’d be fun to link to a random blog post also.
I came up with a component that scrolls through a random selection of items with an animation like a slot machine. Svelte has some pretty cool features for supporting transitions and animations, so I wanted to learn more about that.

You can play with the final result here.
As a TODO for myself, maybe I’ll extract this as a reusable component and publish it.
Web Framework Blues
Jekyll → Gatsby → SvelteKit → Astro
From callbacks to promises via currying
Saucy code
Recently, I’ve been working on some social and gamification features in a web application. I decided to use a backend service called brainCloud instead of implementing all of the database stuff myself. The brainCloud service stores things like player statistics, leaderboards and social relationships (i.e. who’s friends with who?), and provides an HTTP API to read and write the data. They also provide a Javascript client library to make it easier to work with those APIs.
postScoreToLeaderboard(leaderboardId, score, (resp) => {
if (resp.successful) {
// do something with the data
console.log('We got data!', resp.data);
} else {
console.log('We got an error!', resp.error);
}
});
const _postScoreToLeaderboard = (leaderboardId, score) => {
return new Promise(resolve, reject) => {
postScoreToLeaderboard(leaderboardId, score, (resp) => {
if (resp.successful) {
resolve(resp.data);
} else {
reject(resp.error);
}
});
});
};
try {
const data = await _postScoreToLeaderboard(leaderboardId, score);
// do something with the data
console.log('We got data!', data);
} catch (e) {
console.log('We got an error!', e);
}
getLeaderboardTop(leaderboardId, (resp) => {
// handle response
});
incrementUserStats(statId, value, (resp) => {
// handle response
});
// TOO MUCH CODE!
const _getLeaderboardTop = (leaderboardId) => {
return new Promise(resolve, reject) => {
getLeaderboardTop(leaderboardId, (resp) => {
// handle response
})
});
};
const _incrementUserStats = (statId, value) => {
return new Promise(resolve, reject) => {
incrementUserStats(statId, value, (resp) => {
// handle response
})
});
};
const add = (a, b, c) => a + b + c;
// create a curried function
const curriedAdd = curry(add);
const addOne = curriedAdd(1);
// this is equivalent to calling add(1, 2, 3)
const x = addOne(2, 3);
// x is 6
// we have "used up" two out of three parameters
const addSeven = addOne(6);
// this is equivalent to calling add(1, 6, 10)
const y = addSeven(10);
// y is 17
const _incrementUserStats = curry(incrementUserStats);
const fn = _incrementUserStats('statId', 15);
const handleResponse = (resp) => {
// do something
};
// this is equivalent to calling
// incrementUserStats('statId', 15, handleResponse)
fn(handleResponse);
const makeAPICall = (fn) => {
return new Promise((resolve, reject) => {
fn((resp) => {
if (resp.successful) {
resolve(resp.data);
} else {
reject(resp.error);
}
});
});
};
try {
const data = await makeAPICall(_incrementUserStats('statId', 15));
// do something with the result
console.log('We got data!', data);
} catch (e) {
console.log('We got an error!', e)
}
// curry all of the functions that we want to use
const _postScoreToLeaderboard = curry(postScoreToLeaderboard);
const _getLeaderboardTop = curry(getLeaderboardTop);
const _incrementUserStats = curry(incrementUserStats);
// define the function that receives a callback
// and executes the original function
const makeAPICall = (fn) => {
return new Promise((resolve, reject) => {
fn((resp) => {
if (resp.successful) {
resolve(resp);
} else {
reject(resp);
}
});
});
};
// now we can make API calls like this
const result1 = await makeAPICall(_postScoreToLeaderboard('id', 100));
const result2 = await makeAPICall(_getLeaderboardTop('id'));
const result3 = await makeAPICall(_incrementUserStats('statId', 15));




