Hi everyone, and welcome back ๐ And a special hello to my four new followers. Thank you for being here!
A quick map of how this space works: Mondays are for builds. These posts are a little more structured, come with code, and show how things on samira-ray.dev actually work. Fridays are Unstructured Data: The Human Factor, my personal series. If that's what you're here for, the next one goes up this Friday, and it's a conversation I'm really excited to have.
Either way, this whole space runs on conversation. Questions, pushback, "I'd have done it differently": all of it is welcome, on the code posts too.
Now that the housekeeping is out of the way: this started as a therapy project. Well, it started after a therapy session. (The jar didn't need therapy. I did.) One of the things I took away was to make more room for "me time," and for me that meant short writing breaks just for myself. So I bought a pack of journaling and story prompts. I write by hand in a physical journal, and I plan my life in a gamified Notion planner (a template I use, not one I built; credit coming in a minute), so the idea was simple: grab a prompt, write it on paper, and log it in Notion afterward if I felt like it.
What got in the way was picking the prompt. With hundreds to choose from, I'd scroll, second-guess, scroll some more, and by the time I'd chosen one my break was half over. I didn't want to choose. I wanted something like a magic 8 ball: shake it and write whatever it gives you.
So I built a genie jar you shake, and I borrowed its rules from a game I played at the back of class as a kid.
The idea: MASH, but for writing
If you grew up in the 90s or 2000s, you probably played MASH: write a list of options, pick a "magic number," then count around the list crossing off every Nth item until one survives. Mansion, apartment, shack or house. It turns a decision into a little ritual, and the result feels like fate instead of a choice you have to defend.
In my version, your energy decides the categories, and prompts take the place of mansion, apartment, shack and house.
The Prompt Genie Jar works the same way:
- Pick your energy. Low, Steady or High.
- Shake the jar. You get a random magic number.
- Category round. The jar counts around a list of genres, crossing them out until one is left.
- Prompt round. It deals five prompts from that genre and does it again.
- Write whatever's left standing.
The energy step was the part I cared about most. Some days you have a thriller in you. Some days you have a journal entry, and that's fine.
That idea isn't mine, and I want to be upfront about it. The energy levels come from Dream Life Meadow, the RPG-style Notion planner template I use. I didn't build it; it was created by Elle_Sthetique (also on YouTube). It has you check in on your energy and match your quests to it. It worked so well for my to-do list that I wanted the same thing for my writing.
Version 1: a jar for my journal
The first version was a single HTML page that only I used. The prompts lived in the page as structured data, each with a number. The jar picked one, I wrote it in my journal, and if I wanted to log it, the number made it easy to find and tick off in Notion.
Each prompt was just three fields:
{ n: 42, genre: "Mystery", prompt: "..." }
Keeping the data that plain turned out to be the best decision in the project. Everything else (filtering by energy, skipping used prompts, the MASH rounds) is just code reading those three fields.
Energy levels: tag the genres, not every prompt
Borrowing the energy idea raised a data question: what exactly gets the energy level? My first instinct was to tag every single prompt. Then I realized energy is really about the kind of writing, not the individual prompt. A memoir prompt is a low-energy sit-down; a dystopian world is a high-energy build.
So the energy tiers map to genres instead. Low and Steady are written out by hand, and High is everything that's left:
const TIERS = {
Low: ["Self-Help", "Memoir", "Mumblecore Fiction (Bonus)", "Kid Lit", "Biography"],
Steady: ["Literary Fiction", "Contemporary Fiction", "Romance", "Comedy",
"Magical Realism", "Middle Grade", "Historical Fiction", "Young Adult"]
};
const allGenres = [...new Set(PROMPTS.map(p => p.genre))];
TIERS.High = allGenres.filter(g => !TIERS.Low.includes(g) && !TIERS.Steady.includes(g));
Computing High instead of listing it means that if I add a new genre to the data, it lands in High automatically, and the jar never silently drops it. That works out to 5 Low genres, 8 Steady and 13 High.
The MASH loop
This is the heart of the jar. It counts around the list n times, crosses out whoever it lands on, and keeps going until one item is left:
async function mash(el, n) {
const lis = [...el.children];
let alive = lis.map((_, i) => i), pos = -1;
while (alive.length > 1) {
for (let c = 0; c < n; c++) {
pos = (pos + 1) % alive.length;
const li = lis[alive[pos]];
li.classList.add("tick");
await sleep(n > 6 ? 90 : 130);
li.classList.remove("tick");
}
lis[alive[pos]].classList.add("out");
alive.splice(pos, 1);
pos--;
await sleep(220);
}
lis[alive[0]].classList.add("win");
return alive[0];
}
Two details made it feel right:
-
aliveis a list of indexes, not elements. Crossing something out removes it from the count but leaves it on screen with a strikethrough, exactly like crossing it out on paper. -
pos--after a removal. When an item is spliced out, everything after it shifts left by one. Stepping back one position means the count picks up on the right item instead of skipping one.
The magic number is between 3 and 8, and bigger numbers tick a little faster so a long count doesn't drag.
Respecting reduced motion with one line
The whole jar is built on small pauses: the shake, each tick, each cross-out. Instead of adding a reduced-motion check to every animation, I put it in the one function that every pause goes through:
const sleep = ms => new Promise(r =>
setTimeout(r, matchMedia("(prefers-reduced-motion: reduce)").matches ? 0 : ms)
);
If someone's device asks for reduced motion, every wait becomes zero. The rounds still happen and the result is the same; it just appears instantly instead of being performed.
Skipping prompts you've already written
The jar remembers which prompts you've marked as written, so it doesn't keep handing you the same ones. It's saved in the browser with the same guarded localStorage pattern from my last post, so there's no account needed:
let pool = PROMPTS.filter(p => p.genre === genre && !used.has(p.n));
if (pool.length === 0) pool = PROMPTS.filter(p => p.genre === genre);
That second line is the safety net. If you've written every prompt in a genre, the jar reuses them instead of showing you an empty round.
Going public meant writing my own prompts
Once the jar worked, I realized it wasn't just useful for me. Plenty of people freeze at the blank page, and a tool that picks for you takes that pressure off. So I wanted it on my portfolio for anyone to use. That's when I hit the real blocker, and it wasn't technical.
The prompts in my personal version came from the pack I'd bought, and its license was for personal use only. I could use it for my own journaling, but I couldn't put it on a public website.
So the purchased prompts stay in my Notion, and the public jar runs on 515 original prompts instead, across 25 genres plus a Mumblecore bonus round. Writing a fresh set also let me shape them on purpose: they're gender-neutral, so anyone can step into them. And because I typically land somewhere on the gray-romantic and demisexual side, I wrote the romance prompts to work for people like me too. Not every love story has to start with instant attraction, and I wanted to see myself in the prompts I was writing.
It's an easy thing to skip past when you're excited to ship, but checking the license before publishing someone else's content is part of building something real.
What's next
The live jar now sits in the Arcade on samira-ray.dev, next to Ghost Write, a blind free-write tool: you type or dictate and can't see the words, so you can't stop to reread and edit. I first built it for writing into Notion, and it gets its own post. Next on the list:
- Turning both tools into fully working full-stack apps, with the prompts moving from a built-in list into a Supabase database.
- Saving progress per user instead of per browser.
- Giving the writing tools their own home: a small standalone site of writing tools with the same early-2000s feel.
What I'd take from this
- Build for how you actually work. Mine started with paper, not a screen.
- Start with the data. Three plain fields made every feature after them easy.
- Tag at the right level. Energy belonged to genres, not to 500 individual prompts.
- Derive what you can. Computing the High tier means new genres can't fall through the cracks.
-
Put accessibility in one place. One
sleepfunction handles reduced motion for every animation. - Check the license before you go public. "I paid for it" isn't the same as "I can publish it."
Go shake the jar at samira-ray.dev and tell me what you wrote. ๐ซ
And since conversations are the whole point here: did you play MASH as a kid, and what did you always end up with? If you're a builder, I'd also love to hear how you'd have handled the elimination loop. Drop it in the comments ๐
See you Friday for The Human Factor.
A note on how I write these: the ideas, stories and code are mine, and I do most of the writing. My brain moves faster than my fingers, so I use AI to help me put it all together.
I'm Samira, a data science student building AI chatbots, data tools and full-stack apps, especially for customer operations. Data brain, frontend hands.
Top comments (0)