I've been working on a project to make a better festival programme app.
As a festival attendee, I've always felt that there's room for a better programme design that presents all the information we need to plan our day, in an easily digestible format. I have a friend who organises a couple of festivals and I suspect there is an opportunity to make their life easier too, as they do the tricky scheduling.
The project was started mainly as a fun learning exercise. If it ever becomes usable in real life, that will be a bonus.
The public demo is live: festivals-staging.grjacob.com. It is a prototype, incomplete and still undergoing significant change.
Around the IaC setup for GCP and CloudFlare, Claude made two decisions that I needed to intercept. One was a risk I had read about online recently, so I was alert to it. The other was on a less familiar topic.
Claude is happy to store secrets in files that are gitignored, but a gitignore file could easily be changed accidentally, so is not a sufficient safety measure.
Claude was going to allow a certain secret to enter terraform state, which might be a pragmatic choice in certain circumstances, but I would be keen to avoid it if a reasonable alternative solution exists, which it apparently does.
Even after it knew the 'no secrets in state' requirement, its initial implementation still had the exact same problem, which was luckily caught by its own /code-review skill.
Secrets in gitignored files
It may have been a post on LinkedIn where I saw a recommendation to be vigilant against this practice, as coding agents seem to like doing it. Only a few days later, I saw my own agent do it.
The reasons for avoiding it are obvious:
Main reason: A gitignore file could easily be changed without awareness of a serious consequence: your secrets being pushed to GitHub. If the repo is public, that's a disaster. Right now my repo is private, but the code project has no control over that and it isn't a suitable security measure to rely on.
But also: The security and privacy of the dev machine where the code project is checked out is not under the control of the project itself, so it would be safer not to rely on it. I have more peace of mind if project secrets are simply never saved in text on my machine, or the machine of a hypothetical future collaborator.
Claude had initially planned to put a secret into a gitignored tfvars file. To be fair, some reseach suggests it's not uncommon to gitignore these, which was new to me. On recent projects (where the IaC was already set up), I've seen a tfvars file per environment, stored in the repo, without only non-sensitive environment settings in it. Whether tfvars are ignored or not, it still seems to be recommended practice to put genuinely sensitive secrets somewhere safer, like the cloud platform's secret manager or a separate encrypted vault.
Secrets in terraform state
CloudFlare needs some request header settings, one of which involves a secret value. Not a super sensitive one, but a secret nonethelss. When terraform reads this particular type of config from CF, that secret arrives in plain text and is saved in the terraform state, in GCP. This is unavoidable, apparently, as long as terraform manages the ruleset. Claude's initial suggestion was for terraform manage it. Research suggests that some organisations accept this type of risk pragmatically, because their terraform state is encrypted and is access-controlled. I wanted to explore options that would not involve accepting the risk.
Claude initially thought it could avoid the secret being saved in state by using the right terraform syntax, but luckily its own /code-review skill discovered that this was untrue.
I asked for another option, and apparently there is one, although there is a trade-off. The procedure for creating or rotating the secret already involved a script that was separate to terraform. If we give that script a bit more work to do, then we can take management of this specific config away from terraform entirely.
The downside is that a little bit of config is now not in terraform, so that's a little more complexity and a little less neatness. The Terraform plan won't be able to spot any changes to it, expected or otherwise. I'm not 100% sure whether it's the right call, but this is an experimental project and I'm trying to err on the side of caution. If it needs a change in future, I'm sure Claude will happily oblige.
The prototype is heading to the cloud, but the AI-assisted pace is such that I don't feel the need to cut a lot of corners. Indeed, trying to do everything 'properly' and quickly is part of the point of the exercise, and it wouldn't be such a learning experience if I didnt. I'm doing IaC, pipelines, security measures, cost controls, etc, etc, right away.
Requirements:
Publicly accessible, so I can show my friends.
Professional-grade hosting solution - part of the point of the learning experience.
Also, professional CI/CD and dependency management. I want operational steps to be reliable and automated. I want vulnerable packages to get patched.
Absolutely minimal cost - that means staying within free tiers wherever possible.
Risks:
Unexpectedly high usage (potentially malicious) and cloud costs.
Security breach of cloud accounts.
Breach of terms of service for MET Norway's weather API.
They want low traffic, i.e. request data once from back end, save it, serve to all users. Don't let the web app call the MET API directly.
Also, don't let my weather API be exploited as an unlimited public gateway to the MET API for any forecast in the world. Limit it to the festival locations that app needs.
Some considerations and solutions:
The GCP choice is mainly based on the fact that my experience is all in AWS, and I want to learn something new.
The weather feature absolutely requires a back end, to avoid any unnecessary calls to the MET API. It adds a lot more complexity than if I didn't have the weather feature in the prototype. However, this feature is one of the unique aspects of the design, so it's important. Future features will need a back-end anyway.
One of the complexities is that I can't use Firebase's 100% free, but limited offering, because it lacks the right kind of back-end integration. I need the full package, which still has a free tier, but high usage will incur costs, rather than simply taking that app offline.
To minimise cloud usage costs, I want lots of caching. Putting CloudFlare in front of my primary hosting solution (likely GCP) adds extra caching and extra protection against misuse, for free, reducing the chance of unexpected GCP usage.
I also want usage and cost alerts, an easy manual killswitch and ideally a fully automated killswitch, to completely take the service offline if surprise usage is about to rack up a bill.
Security is a big one. Claude is happy to take some calculated risks and cut a few corners, but I'm not. Gitignore is not sufficient security (a common problem with AI-assisted development, apparently). Secrets should be in Secret Manager, not in terraform state unless absolutely unavoidable. Terraform state should be encrypted. Etc, etc.
I worked with Claude to formulate a researched plan and broke it into tasks, which I'm working through one-by-one.
Here's the high-level task list:
1. GitHub CLI and repo settings.
2. CI, no deploy. (build, tests, dependabot)
3. The weather API, local. (The minimal back end.)
4. Docker container for the weather API.
5. Deeper research, confirming various open questions on CloudFlare and GCP functionality, config, costs, alerting, etc.
6. Bootstrap Google Cloud: a Google account for this project with two-step verification, the billing account, the project, the budget, the state bucket, the CLIs; the region and IDs settled.
7. Create a Cloudflare account and buy a custom domain.
8. Write (most of the) Terraform for GCP and CloudFlare.
9. Usage alerts in GCP.
10. Update privacy notice and 'about' page.
11. First deploy by hand.
12. Deploy pipeline.
13. Run terraform plan in CI for PRs touching infra.
14. Replace documented plan with documentation of what's built.
15. Killswitches: automatic and manual.
I'm half-way through this task list now. Interestingly, and somewhat predictably, it's common for a task to reveal new information or new considerations, resulting in adjustments to the future plan. This is essentially why I think that, despite the high speed of implementation with AI, agile isn't dead and traditional waterfall isn't back on the table - it seems fundamentally true that a theoretical plan or design contains uncertainty and assumptions, which are only resolved during the execution. This means that the broader the scope of a theoretical plan, the more likely it is to be wrong or incomplete - simple really. The agile approach is to expect this, keep options somewhat open and embrace the forces of evolution as work progresses, rather than attempt to stick religiously to some theoretical spec that was never going to be optimal. Of course AI changes what's possible, but I'm not sure it's changing this fundamental truth, which rules out waterfall as a valid concept in my view. I've been writing about this in a separate article, inspired by the 'is waterfall back' chatter online.
Understandably, Claude's work has become slower and more token-costly as the project has grown. I've been looking for optimisations to keep token usage down.
Whereas 'clean code' architecture made a lot of sense pre-AI, it now comes with the cost of more files being opened, read and edited when an AI agent makes changes, meaning more in the context window and more token usage.
Shifting to 'vertical slice' architecture could be useful here, as well as breaking up larger, frequently touched files.
'Clean architecture' splits a code project into layers: domain, application, interface adapters, frameworks and drivers. There is a neat separation of concerns, which lowers certain risks. You could swap out parts of your modular outer layers (like a DB) without your core domain logic being touched at all.
However, that means that a given feature involves multiple code files down the layers - the feature is a vertical slice. Understanding a whole feature so you can change it, or adding a new feature, involves interacting with all of those layers, which could be a lot of context.
If the code project is broadly architected by vertical slice, instead of by horizontal layer, then a feature probably has one directory (maybe only one file) with most of its implementation and little/no implementation relating to other features. There can still be shared files that serve multiple features - you don't want to be duplicating important code, like broad domain logic, which should obviously only be written in one place. Still, the result is probably fewer files and fewer lines to deal with, when analysing, editing or writing a feature.
So, Claude created new directories by feature and shifted the necessary code into those directories. When it goes looking for those features in future, it'll find most of what it needs in a single directory, nothing superflous or unrelated.
Shared domain code is still in its own directory, already split into sensibly themed files.
Also, some bigger feature files have been split up, where they effectively define sub-features. When one of those sub-features is edited in isolation, Claude will have much less code to read.
Time will tell whether this slows down rate of token usage.
Lots of progress recently...
🖌️Festivals can set their own brand colour, which affects the colour scheme of the app. It works like this: https://material-foundation.github.io/material-theme-builder/ N.B. users can also change between light and dark mode.
🌄There are now four timeline backgound options, each of which has both a light mode and dark mode.
'Natural skylight': the original background already in place, but now with a dark mode variant. It has a yellow sunlight tint by day, a warm sunset and a blue-grey night. The dark mode is simply a dimming of all the colours involved.
'Tinted skylight': this uses Material Design surface colours, darker by night, and a noticeable coloured tint at sunset and sunrise. The surface colours appear to be greyscale, but are slightly tinted to the theme. The sunrises and sunsets have a more distinct colour from the festival's brand colour scheme. If a festival organiser likes the idea of the rich background that follows the sun, but finds the original background clashes with their colour scheme, then this might be a nice option.
'Simple skylight': No added colour at sunrise and sunset - just a simple fade from a lighter to a darker surface colour. No worries about colour clashes, but the daylight feature is still there.
'No skylight': A plain background at all times, using the recommended Material Design surface colour. Light mode and dark mode are a very light grey and a very dark grey respectively, slightly tinted to the app colour scheme set by the festival.
⭐'My events' (starred) can be filtered on the timeline. There's a button that cycles through three filter states:
Off (default): All events are shown in their normal colour. A 'starred' event is distinguished by its star button being 'on'.
Highlight starred events: Starred events keep their colour, all other events go to greyscale. (Actually, the greyscale might be tinted towards the app's colour scheme, but it looks roughly grey to the eye.) This mode would be helpful while forming your plan, as you can very easily see what you have added, as well as what you haven't added (yet).
Show starred events only: Anything not starred is completely hidden. Any venue rows with no starred events are hidden too.
Cycling through more than two states on a Material Design floating action button (FAB) is not standard for the design system - it's normally a binary toggle, or a multi-option menu. However, my feeling is that users will probably get along with it. I'm thinking of the Spotify button that cycles through three 'repeat' modes (off, all songs, one songs). Effectively, it's a single action button that means 'go to next mode'. I might need to tweak the design further (e.g. maybe three dots, one highlighted, showing a progression through the options), but I don't think the three-option cycle needs to be ruled out.
Light mode, 'tinted skylight', highlight starred events.
Dark mode variant of the 'natural skylight' background. This festival sets a custom brand colour, affecting the theme of all of the app elements beyond the timeline background and timeline events.
It's time for a custom colour system for the event cards on the timeline, because the standard Material Design framework doesn't quite work. Google itself expands beyond its own design system and my heavy inspiration here is the Google Calendar, in which events can use a range of colours.
As a starting point, I'm using the Google Calendar colour set - 24 colours that include intense, punchy ones, as well as more subtle ones. Festival organisers can pick a set of colours that suits their festival's brand and personality. The colour set can also be expanded in future.
The app takes the seed colour for each venue and applies a custom colour role system to determine the colour of various pieces of text, the 'set' boxes, the star button in its two states, etc. The event card surface is always the exact seed colour, but deriving the other colour roles is a bit more complicated, because some seed colours need light text, some need dark text, etc. The custom colour system aims for accessibility in contrast.
The screenshots show my three sample festivals using different venue seed colour sets.
Default: varied, colourful.
Warm and strong.
Soft, cool, gentle.
I've been looking at how various modern apps handle menus, nav bars, nav rails, search boxes and so on, to work out how to structure the app beyond the timeline. I'm mostly applying the guidance in Google's Material Design (version 3), but, interestingly, I've seen that some mainstream Google apps diverge from that design system slightly, so I think I can too.
Here's how it looks so far on mobile. It's a little bit different on tablet/desktop, which I'll show in later screenshots. The exact contents of the navigational elements will change further, but the basic structure is coming together.
The design so far included some shading to distinguish between venue rows, but I'm not sure we need to show the venue rows.
On narrow mobile screens, it's clear which venue each event belongs to, because the events are never far from the venue label. At festivals, almost all audience members will probably be using narrow mobile screens, so I want to design primarily for them.
As long as there is a certain density of events on the timelines, those events themselves indicate rows, even on wider screens.
In these scenarios, it doesn't feel like we need additional visual indicators to signal venue row information. The advantage of losing them is a cleaner design and a little less graphical work for the device to do.
However, the lack of row indication might be annoying in the scenario where event density is low and a big desktop screen is being used. An example might be when a festival organiser is creating a programme, but hasn't added many events yet.
Options include showing row formatting on larger screens only, or perhaps letting users turn it on in a setting. There are various ways rows could be formatted and I've been trying some out with a temporary design tool built into the app. No decisions yet - one to come back to later.
Row formatting might be useful on big screens when event density is low.
The web app would need to work well with no internet connection, or a flaky one, in a real festival scenario. This means it needs a 'service worker'. Also, I think it makes sense to make the web app installable with its own icon on the home screen and neat separation from the clutter of other browser tabs. This can be achieved by making it a 'Progressive Web App' (PWA).
These changes are now working nicely. For the service worker, it is using 'Workbox', via a Vite plugin. Now that it's a PWA, the browser menu offers the option to install it for an experience close to that of a native app.
The service worker handles data and app code updates, for example not presenting a change while the user has the app open, as that would be confusing, but updating things next time the user looks. There are some 'snackbar' messages informing users about updates and there is option to refresh on demand. Those messages, plus a new online/offline/poor connection status, will need UX tweaks down the line, but the basic functionality is there.
These images give an overview of the programme timeline in a few scenarios. New features include:
The 'now' line. When a running festival's programme is opened, 'now' will be on-screen, shifted left a bit, to show a bit more of what's coming up than what's just been.
The star button for adding events to the user's personal plan. It isn't wired up to a 'my events' list yet - that willl be the easy bit. Getting it to appear in a reasonably neat way on a variety of card shapes at various zoom modes was the hard bit.
Star button, high zoom.
Sunset, low zoom.
'Now' and weather.
The layout of information in event cards was proving very tricky, so I prompted Claude to build another design tool into the app itself. Just like the colour-picker tool, its code is neatly separate from the core app code, so can be easily removed later.
The tool made it easy to make minor adjustments and see the impact immediately. This is obviously much easier and quicker than telling Claude 'up a bit, down a bit'.
An alternative would be to make the adjustments directly in the code and I would do that if it were straightforward, but in this case it's not. The reality in the code is more complex than the design tool's simple list of adjustable parameters suggests. An abstraction from that complexity is welcome. For example, changing one parameter in the design tool might result in derived changes to multiple real parameters in the code.
The layout of the cards is now getting a bit better. The adherence to Material Design is still pretty loose, but this is far from a normal application of 'cards', so I'm treating MD's guidelines as a starting point rather than a goal. Elsewhere in the app, where I expect features to be more conventional, I still intend to follow MD closely.
Struggles with event colours are ongoing. I am trying to adhere to Material Design where I can, but it does cause some complications, although hopefully smaller ones than coming up with a whole design system from scratch at this early stage.
In Material Design, you start with a 'seed colour' and from there it calculates a whole colour scheme, telling you which colours you can use for fills, buttons, text, etc, in a whole range of scenarios. See: https://material-foundation.github.io/material-theme-builder/
However, one colour scheme only gives you three different fill colours for containers (like cards), plus a red error colour. Most of the fill colours in a scheme are very pastel, even if your seed colour was not. The design system assumes white/grey/black backgrounds, not the novel daylight/night sky background in this app.
The event timeline probably needs more colours and different colours to those provided by a single Material colour scheme. As it stands, the app sets a seed colour for each venue, then the other colours are derived from there. This isn't really how Material is meant to be used (multiple seed colour schemes on the same page).
After many failed attempts to elicit an attractive set of venue/event colours from Claude, I got it to build a colour-picker feature into the app. One of the advantages of AI-assisted development is that design tools and coded apps no longer need to be separate entities - we can feasibility put design tools directly into the app itself, then take them out later.
After much back-and-forth, a set of venue/event colours has been achieved that is fine for now, but still not ideal. I think a future task will involve creating a more custom design system for the event cards.
Best effort so far, within the self-imposed constraints of Material Design, using the 'primary container' colour role for most fills.
A colour-picker feature now built-in.
A material colour scheme for the seed colour shown above. Note how muted everything is, apart from the error colours.
Claude struggling to come up with colour sets that look nice.
Breaking the Material Design rules with 'primary' colours applied to card fills - not good.
I dug into the Material design system and found out what I needed to do to neaten things up. Once I knew Material's concepts and terminology, both the design decisions and the prompt writing became a lot easier.
Better adherence to Material has significantly neatened up the 'cards' that represent events and sets. Further tweaks around typography and colour will be easier, helped by the framework.
The colours are still not beautiful, but it should be more straightforward to try other colours now. From one 'seed' colour, Material defines a series of colour variations based off it, which can be used in different 'roles', like backgrounds and text. So, I just need to allocate one new 'seed' colour to each venue and the 4 or 5 variants of the seed colour will automatically apply to the cards.
Using Material cards of various seed colours against my custom daylight/night sky background is probably quite uncoventional and adds a bit of a complication - we need to harmonise the day colour, the night colour and several venue seed colours and the seed colour for the wider app beyond this screen (barely in existence yet). I expect a lot of colour experimentation and iteration before it feels right.
High zoom, daytime.
Very low zoom, day to night.
New feature: at very low zoom, we now see the timing of 'sets' (within events) represented as horizontal bars at the bottom of the event card. Styling may be tweaked further, but I think the principle is right.
Several other little improvements came along with this design iteration, including:
Adjustments to typography (font size, weight, width and colour role allocation) for the various components on the cards (title, artist, timing, description). For example, the title used to be bolder, but it was a bit ugly. It is now given more prominence without excessive weight, by using a slightly darker Material colour mode (on-primary-fixed) than the rest of the card text (on-primary). The artist name and event timings are given slightly more prominence than the description by using a slightly higher font weight, although the font size is the same. The descriptions also use a very slightly tighter character spacing. There's more to adjust here, but it's getting better.
Timings are now shown in some zoom mode/space availability scenarios where previously they were not.
If there isn't space to show the full timing ('1:30 pm - 3 pm') then just show the start time ('1:30 pm'). Something is better than nothing. If there isn't even space to show that, then show no timing, as before.
Real forecast data is now shown in the header next to the time labels, if the festival is within forecast range.
The weather data comes from MET Norway, which offers a free API with generous usage terms. They do ask for accreditation, which I've added to the bottom-right of the screen, similar to how Google Maps credits the providers of satellite imagery.
Before I could make this, I needed to guarantee that the prototype would always have a sample festival that is happening now. I generated a fresh sample festival that is similar to the existing ones, except that some dedicated app code will dynamically change the dates on load. It always starts yesterday and runs for 9 days. To avoid sample data bloat, the event density is lower than for the existing sample festivals and decreases over the course of the festival - not entirely realistically, but fine for checking the forecast feature several days ahead.
The weather data has hourly forecasts for the first three days, then forecasts for 6-hour periods after that. The app doesn't show every hour label when zoomed out, so the time of the forecast might not correspond to a time that we are actually displaying. The solution is simply that the weather info is shown alongside the first hour label that falls within the 6-hour forecast period - this seems more than good enough for a few days in the future, when all we want or expect is a rough indication of hot/cold, wet/dry. We'll get a better hourly forecast closer to the time. What matters most for the festival use case is the next several hours (not days) as we choose between indoor and outdoor activities and decide whether to take a sunhat or an umbrella.
The forecast data includes a weather symbol code like clearsky_day or heavysnowshowers_polartwilight (it's Norwegian). These codes can mostly be easily mapped to Google Material icons, although there were a few exceptions that needed some attention. Surprisingly, some of the Material icons are quite unintuitive. The main precipitation icons ('rainy heavy', 'rainy snow', etc) are just lines and dots. A more familiar icon is a conventional cloud with raindrops or snow blobs falling from it, so I've mapped to that instead, but unfortunately there are no variations for the heaviness of the rain. A future improvement would be to find a different, better set of weather symbols. One option would be to use MET Norway's own symbol set, but I think I want something more clean and minimalist than this.
Mainly for testability, I've added a tooltip on hover over the weather symbol, which tells us the original MET Norway weather symbol code (clearsky_day). This lets me test that the symbols I am seeing are well-matched to the description. There's probably no production use case for the tooltip. You don't get the tooltip on mobile, which is probably the primary method of usage, i.e. during a festival. Desktop usage, such as programme creation by festival organisations, is more likely to happen well in advance, when there isn't a forecast anyway.
Much tweaking has led to better distribution, wrapping and truncation of the various pieces of information that can be displayed in event blocks on the timeline. There are a few complications that made it tricky:
Every event (and 'set' within an event) has a title and timings, which we show.
There might also be a linked artist (optional), whose name we can show. It is also optional for the artist's name to be used as the event title.
Events and sets can have descriptions, which are optional, but nice to show when space allows.
There is a priority order for these pieces of information and they have different rules about whether they can be truncated or wrapped.
The app currently has four different zoom modes, which display different amounts of information over different numbers of lines. For example, at the highest zoom, we can show all four of the pieces of info mentioned and we can try to distribute them over up to 5 lines. At the lowest zoom, we can only really show the event title and maybe the timings, all in one line.
Different events and sets obviously have different lengths, so the boxes are all different sizes. Plus, the lengths of the text strings are all different.
At high zoom, for long-running events, we don't want the text to run across the whole box - we need to wrap it early so that it's readable, particularly on narrow screens.
The design is meeting a few user needs/wants now, but there are a few more coming up. As a prospective user myself, I'm imagining what information would be helpful to have in one place.
Morning coffee outside the tent, planning the day ahead...
✔️ In place:
What's on where and when. (In theory, all festival programmes do this already.)
Event overlaps. (Easily visible thanks to the timeline. Not unique, but not common.)
Will it be dark or light out? (Unique AFAIK.)
Maybe coming soon:
Weather! (Unique AFAIK.)
Event and artist details. (Standard.)
Artist streaming links/embedded players.
Personal festival plan. (Mark the events you're interested in and filter for a view that only includes them. Becoming more common in digital programmes.)
Around the festival during the day...
Ideas, maybe for later:
Where are the venues? Festival map!
Looking at the map, what's on right now at each venue?
Event feedback/ratings.
Notifications for schedule changes.
Messages from organisers to attendees.
Stretch ideas (far future):
Social - friends' festival plans, location sharing. (See which of your friends are at which venues right now.)
For the festival organiser...
Probably coming soon:
Editing.
Maybe later:
Broadcast messages/alerts to attendees.
Stretch ideas (far future):
Organiser insights - crowd distribution (from phone GPS), event attendance.
Festival crew management.
While visiting my parents, I checked the web app on one of their phones, which is in super-big-text readability mode. The layout and readibility were absolutely fine, thankfully.
There was a surprise problem though: the graphical performance was terrible while scrolling and zooming around the timeline. The phone under test is apparently much less powerful than mine, on which the experience had been completely smooth.
The good news is that Claude found several performance optimisations, which seem to achieve about 70% less work per frame while scrolling, and 30–40% less around a pinch-zoom. (Numbers come from experiments on a CPU-throttled browser.)
It was probably Claude's most time-consuming task on this project so far to analyse the performance, find opportunities for improvement, do the fixes and test them, but it showed its value here. Low-level performance analysis of a web app would be a very tedious, fiddly job to attempt by hand. It would have been even better if Claude had avoided the problems in the first place, but it's probably inevitable that a few problems like this will emerge in early development, as they would with manual development. At least they are easier to fix now.
When the clocks go back in the autumn, 01:00 happens twice... the clock goes to 01:59 BST, then goes to 01:00 GMT, then carries on as normal.
To avoid confusion (hopefully), I've included the timezone in the header's time labels for the hour before and the hour after a clock change.
In theory, the solution should work when the clocks go forward in spring and should work in other parts of the world too, although there is a little bit of complexity around how timezone labels will appear in different users' locales. So, that's something to test in more depth at a later stage. In the spirit of an early design and discovery exercise, this is good for now.
To do: test spring forward, test non-UK locales.
To do (still): sort out the styling of the time labels in the header. Make the time format consistent in the events.
Claude added the '+1' to the event times where an event crosses a midnight boundary, indicating 'next day', like on a plane/train ticket, but I'm not sure it will mean much to users here. I'll probably drop it. I will probably experiment with showing the timezones in the events that cross such a boundary.
Over many iterations, I set up date selectors that both indicate the currently displayed date and allow users to jump to different dates. The date selectors need to be scrollable left and right, as there may be more of them than can fit on screen. I appreciate that the design of these selectors is a bit different to the 'tab' structure you often see on timelines, such as a weather forecast. However, I am really liking it. I'll have to see if friends get on well with it.
The venue labels also needed some work. I wanted to distinguish them better from the events and make sure they were visible by day, by night and during the sunrise/sunset gradients. They are not perfect yet, but they are better than before.
Something tricky was getting the date selectors to move around in an intuitive way when the timeline is scrolled sideways. I wanted date boundaries (midnight lines) to push and pull the relevant date selectors. The movement of these selectors is something that a user shouldn't consciously notice if works well, but will definitely notice if it feels odd in any way.
A challenge with AI-assisted design/development is putting concepts around dynamic UI behaviour into words. We got there, but I imagine there might be cases in future where it's easier to edit the code myself.
The header (date selectors and time labels) needed to remain visible even when we scroll down the programme, so the solution (for now) is a translucent, smoky screen that appears when needed.
To do: Improve the styling of the time labels, particularly the minor ones ('5:30 pm') so that they stand out against the background better, while still remaining subtle compared to the primary info on-screen (events and date selectors).
To do: use a consistent time format for the header and the event details. Simplified 12-hour ('6 pm') is probably best for the audience, even though I quite like using 24-hour time formats as a techy.
At this point, about 1 day's worth of intense Claude-assisted work is done and the app is feeling pretty real. If I were to switch the stack, now would be a good time, before it gets painful.
Before the groundwork began, I did some research on stack options, focusing on the UI. Although I was thinking a little bit about cloud infra, deployments, etc, I probably just needed flexibility at this stage and didn't need the distraction of big decisions that were unrelated to the UI design.
Although a smartphone app is on my mind, parallel codebases for native Android and iOS would be probably overkill at first. Flutter was an interesting option that looked pretty good, but the web app option seemed like the best place to start for speed, simplicity and low risk, at least for this initial exercise, which is mainly focused on design. A few thoughts...
I'm only designing straight into code because we can now; pre-AI this exercise would have been an artistic one and code would have come later. It's ok to see this as a discovery and design stage that might become more, rather than a definite end-product development project.
Whether a mobile app will be useful in future, who knows. A web app is simple, portable and almost certainly desirable, whatever comes later.
A future rewrite is always an option, and might even be a good idea anyway, to optimise the codebase and drop any tech debt that might be acquired in the iterative design process. A first implementation can be used as a rich spec from which to write a second, neater implementation.
With some AI advice, I landed on Node, TypeScript, React, Tailwind CSS, Vite (dev server), Prettier, Vitest and playwrite.
The Vite server is great - it's super fast, reloads code changes instantly and I can test on my phone over wifi without exposing to the world. With the app, I haven't encountered any hurdles yet where a tooling choice limits design or functionality options. I already have some exposure to React and can make sense of the code. I'm not a React/TypeScript expert at all, but I'm not starting completely from scratch. I value automated testing and the tooling there seems to be doing a good job so far.
So, with the design coming together, I'm quite happy with this stack and it seems likely that it will stick, at least while project is still a prototype.
A design feature I was excited to try out was a background colour linked to daylight. We have location data for the festival, so we can calculate the exact sun altitude at any time. Apart from (hopefully) looking cool, there could be some practicality here.
Should I take my torch to the concert so I can find the tent afterwards?
What time do I need to turn in, so I can get to sleep before first light?
As a festival organiser, what are my scheduled events going to feel like in reality? Which band is getting the magical golden-hour light? Will it be dark enough for a band's psychedlic light show?
Making the background represent the sky.
I wanted a light warmth in the day and a soft, slightly blue darkness at night, with some golden sunset/sunrise colour in-between. The exact colour styling will be tweaked further in future, but a few iterations achieved a result that I'm very happy with for now.
At first, the fancy background didn't look as slick as I had imagined. Something I learned was that some apps give their backgrounds a slight noise grain, to make them feel soft and smooth. This helps to make gradients look better too, without such noticeable banding. After some tweaks, the grain is imperceptible (as far as I can tell), but the background does feel much nicer.
To do: The colours of the events don't work beautifully with the background yet - this is something to look at in future.
Horizontal timeline, venue rows, background styling linked to sun altitude.
I've always wanted a horizontal timeline in festival programmes and have rarely seen it.
Once the sample festival data was in place, I got the basic structure created and started making tweaks to it.
An early decision was to use Google's 'Material Design' system as a starting point. It's familiar, it's well-established and it advances the UX quite a long way with a one-line prompt. The second screenshot shows the difference.
The first appearance of a UI. Rought structure ok, lots of work needed.
Started using Material Design and making various tweaks.
Context, data model, sample festival data.
Initial goal: just create the UI design for the programme, as this is the important, unique bit. Things like back-end APIs, data storage, user management, etc, are likely to be fairly generic and less interesting, so they can wait.
I'm using Claude to help me develop and one of the first things I did was give it context about the idea, which it added to the project.
Then I did the data model, explaining concepts around orgs, festivals, events, artists, people, etc, and how they would link up. Claude made a nice data-model.md with a mermaid diagram. When I was doing this, I had a few features and design choices in mind, so compatibility was built in.
Most of the data model is pretty intuitive: venues, events, artists. There are a few little notable choices, such as that an event may have multiple linked artists, or none. Festivals will need flexibility. There's also going to be option to associate people with artists and some people might be in multiple bands, or perform solo as well as with a group. One reason for this is to allow festival organisers to spot timetable clashes for individual people.
One special feature of the data model is that scheduled events can have sub-events, called 'sets'. Some festivals or festival stages won't need this - if they have a simple schedule of back-to-back performances all day, they probably just want a series of separate, single-artist events. No need for more complexity there.
However, in the festivals my friend organises, it's common to have an 'afternoon concert' and 'evening concert' at the main stage, each lasting 3 - 4 hours and featuring ~3 artists. Audience members often attend for an entire concert - to them, and the organisers, it's conceptually one event. However, some attendees will flit between different venues, so they want to know the set times within a concert. We could also use this structure for an event with an interval, perhaps with some special sub-event during the interval. An example might be a ceilidh with two halves and a performance by a dance team during the break.
With the data model foundations more-or-less complete, I started creating demo data for two festivals, so that we have something useful to test as soon as app dev starts.
The festival programme will be shown as a timeline, so there are two timeliney risks on my mind, which the design will need to consider:
Big risk: spring & autumn clock changes. One sample festival will need to sit over a clock change boundary - I chose the upcoming October one.
Less worrying: month and year boundaries. Another sample festival crosses NYE, covering both.
In future, I'll want a dynamically generated sample festival that is always on now, but I don't need that for the basic design just yet.
AI was of course excellent for generating the sample data. I described the kinds of fictional venues, events and artists that I wanted, covering a range of scenarios, and it generated the data brilliantly. Obviously, this would have taken a long time to do by hand (coming up with one new band name is hard enough, let alone dozens) and probably would have resulted in unrealistically small sample festivals as my patience ran out, and/or a lot of lorem ipsum. Instead, I have a couple of pretty believable, entirely fictional festivals that have multiple stages and run over multiple days.
Claude wrote itself some documentation about what I wanted from the sample festival data, as context for future tweaks and additions. I could very easily prompt it to generate more fake festivals in future with rich, varied, realistic data and minimal effort.