the dates were never written the same way twice
My friend kept missing deadlines
A friend of mine loves joining competitions. Hackathons, business plan contests, design challenges, anything. So do I. But we kept running into the same problem: by the time we heard about a competition, the deadline had either just passed or was days away. The information was always out there, just never in one place.
It came through Instagram posts with no consistent format, WhatsApp forwards that got buried in group chats, and email announcements nobody saved. My friend started keeping a manual list somewhere. They would update it when they remembered, miss entries when they forgot, and lose track of which guidebook went with which event. When I wanted to share a new competition, I had to send a screenshot and hope they would remember to write it down.
I decided to build something.

The problem was not finding competitions
Once I sat down with the problem, I realized it was not really about a shortage of competitions. There were plenty. The problem was that the information was scattered, and deadline visibility was zero.
Nobody needed another place to post competitions. What we needed was a directory: one URL you could bookmark, that collected everything in one place, let you filter by what was relevant to you, and made it obvious at a glance what was still open. That was the shape of the thing I wanted to build.
Building the directory
Listlomba is a searchable directory of Indonesian student competitions. Each entry has the deadline, the scale (national or international), the category, a link to register, and a link to the guidebook. The cards have live countdown timers so you can see at a glance whether a competition is still open, and a sort by deadline means the most urgent ones always float to the top.
The public side is fully server-rendered with Astro on Cloudflare Workers, which means the page loads fast from anywhere in Indonesia. D1 handles the competition data and R2 stores the poster images, which are proxied through the Worker instead of served directly from R2 so I can control caching and access. There is an admin panel behind a password-protected session for managing the full competition lifecycle, adding new entries, and replacing posters.
I also built a CSV import tool so I could add many competitions at once without entering each one by hand. That turned out to be the hardest part of the whole project.

The dates were never the same twice
When I started collecting competition data from various sources and trying to import it, I ran into a problem I had not anticipated. The date fields were completely inconsistent.
Some entries had a full date like 28 Maret 2026. Others had a range like 16 - 17 Mei 2026 or 04 Mei - 05 Juni 2026, sometimes with a regular hyphen and sometimes with an en-dash. Some had only a month, like Juni 2026, with no day at all. Some had nothing and just used a dash as a placeholder. I also found double spaces in the middle of date strings, and the status column had real typos scattered through it: Winer instead of Winner, Progres instead of Progress, BNT as an abbreviation that needed to be normalized.
I wrote a custom CSV parser to handle all of it. It normalizes Indonesian month names into ISO dates, takes the start date from a range, defaults to the first of the month when only a month is given, and maps the typo’d status values to the correct enum. The field for categories also contained commas inside quotes, so I had to write a proper quoted-field parser to avoid splitting on those.
The part I am most proud of is how the parser handles the display versus the sort. It keeps the original human-readable string ("16 - 17 Mei 2026") in a separate JSON timeline column alongside the parsed ISO date, so the UI shows you what was actually written while the database sorts and the countdown timer counts down using the clean date. Those are two different things and they need to be stored separately.
What it cannot do yet
The most obvious gap is that competition statuses are all updated by hand. There is no scheduled job that automatically marks a competition as expired when its deadline passes. If I forget to update it, a competition that ended weeks ago will still show as open. Fixing that properly means writing a Cloudflare Cron Trigger, which I have not gotten to yet. It is on my list.
The CSV import also does not check for duplicates. If I import the same file twice, it creates duplicate entries rather than detecting that the competition already exists and updating it instead. I handle that manually for now, but it is a real problem as the database grows.
The countdown timers use the visitor’s local timezone rather than Asia/Jakarta time, which means someone browsing from a different timezone will see a slightly different countdown. It has not caused real confusion yet, but it is not correct behavior, and I want to fix it.

The thing I measure it by
Listlomba now has around 70 competitions in the database, and I share the link with friends on campus whenever I hear about something new. People actually come to it when they are looking for something to join.
The metric that matters most to me is not the database size. It is that my friend, the one who started all of this by missing too many deadlines, actually uses it without me asking them to. That was the test I had in mind when I built it, and it passed.