This app will enable participant of Jamboree 2026 to book various activities.
- Gleam
- mist + wisp for web server
- lustre + hx for templating and SSR
- Squirrel for type safe DB interface
- Cigogne for database migrations
- HTMX
- TailwindCSS
- PostgreSQL
| Path | Purpose |
|---|---|
src/ |
Gleam source code |
src/j26booking/ |
Main app modules (components, data, router, sql, web, etc.) |
src/j26booking/sql/ |
SQL queries for Squirrel |
priv/migrations/ |
Database migration SQL files (applied with Cigogne) |
priv/seeding/ |
SQL scripts for seeding the database with example data |
priv/static/ |
Static files to be served by the web server (e.g. HTML, CSS) |
test/ |
Gleam test files |
gleam run # Run the project
gleam test # Run the testsThis guide is for frontend developers working on UI components and styling. No deep Gleam knowledge required to get started!
The whole section is generated by the Claude Code AI tool 🤖 and validated by Markus Wesslén.
- Start the server: Run
gleam runin the project root - View the app: Open http://localhost:8000 in your browser
- Make changes: Edit files and refresh the browser to see updates
- Format code: Always run
gleam formatbefore committing
priv/static/ → Static files (HTML, CSS, JS) served directly by the web server
├── index.html → Landing page (example of pure static HTML)
├── styles.css → Your CSS files (TailwindCSS recommended)
└── *.js → Optional JavaScript files
src/j26booking/components.gleam → Lustre components for server-side rendering
(generates HTML dynamically from database)
For landing pages, static content, or pages that don't need database data:
Example: priv/static/about.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>About Us</title>
<link rel="stylesheet" href="/styles.css">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/htmx.min.js"></script>
</head>
<body>
<h1>About Jamboree 2026</h1>
<p>Welcome to our booking system!</p>
</body>
</html>- Files in
priv/static/are automatically served athttp://localhost:8000/<filename> - Use this for: landing page, help/FAQ pages, static assets (CSS, JS, images)
For pages that display database data or need server-side logic:
Example: See src/j26booking/components.gleam for activities_page and activities_list
- Components are written in Gleam but generate HTML
- Use this for: activity lists, booking forms, user dashboards
- Components can be full pages or fragments for HTMX
You don't need to be a Gleam expert! Here's the basic pattern:
import lustre/element
import lustre/element/html
import lustre/attribute
pub fn my_component(title: String, items: List(String)) {
html.div([], [
html.h2([], [element.text(title)]),
html.ul(
[attribute.class("my-list")],
list.map(items, fn(item) {
html.li([], [element.text(item)])
})
),
])
}Key concepts:
html.div(attributes, children)creates HTML elementsattribute.class("...")adds CSS classeselement.text("...")creates text nodeslist.map(items, fn(item) { ... })loops over lists
- Open
src/j26booking/components.gleam - Add your function (see examples above)
- Use HTML-like Lustre functions:
html.div,html.button,html.form, etc. - Run
gleam formatto auto-format your code
Example - Adding a booking form component:
pub fn booking_form(activity_name: String) {
html.form(
[
attribute.method("post"),
attribute.action("/book"),
],
[
html.h3([], [element.text("Book: " <> activity_name)]),
html.input([
attribute.type_("text"),
attribute.name("group"),
attribute.placeholder("Kår/Patrull namn"),
]),
html.button([attribute.type_("submit")], [
element.text("Boka aktivitet"),
]),
],
)
}- To use it in a route, ask a backend developer to add it to
router.gleam
HTMX lets you build dynamic UIs without writing JavaScript. Key patterns used in this project:
The activities page uses this pattern (see components.gleam:29-61):
// Full page component
pub fn activities_page(activity_names: List(String), search_query: String) {
html.html([], [
html.head([], [/* ... */]),
html.body([], [
html.input([
hx.get("/activities"), // Fetches from this endpoint
attribute.attribute("hx-trigger", "keyup changed delay:300ms"),
attribute.attribute("hx-target", "#activities-list"), // Updates this div
]),
html.div([attribute.id("activities-list")], [
activities_list(activity_names), // Initial content
]),
]),
])
}
// Fragment component (reused in full page and HTMX updates)
pub fn activities_list(activity_names: List(String)) {
html.table([], [/* ... */])
}How it works:
- User types in search box
- HTMX sends GET request to
/activities?q=search_term - Server returns just the
activities_listfragment (not full page) - HTMX updates
#activities-listdiv with new content
html.button(
[
hx.post("/book/" <> booking_id),
hx.swap(hx.OuterHTML, None), // Replaces the button itself
],
[element.text("Book")],
)When clicked: sends POST to /book/123, replaces button with response (e.g., "Booked ✓")
pub fn item_list(items: List(Item)) {
html.ul([],
list.map(items, fn(item) {
html.li([], [
element.text(item.name),
html.button(
[hx.delete("/items/" <> item.id), hx.swap(hx.OuterHTML, None)],
[element.text("Delete")],
),
])
})
)
}Ask your backend developer to return your form component with error messages on validation failure.
TailwindCSS is a utility-first CSS framework that lets you style elements using pre-built classes like bg-blue-500, text-white, and px-4 directly in your HTML/Lustre components. Instead of writing custom CSS for every element, you compose designs using these utility classes.
To set it up using glailglind (a Gleam package for TailwindCSS):
-
Install glailglind (one-time setup):
gleam add glailglind --dev
-
Install TailwindCSS binary:
gleam run -m tailwind/install
-
Configure in
gleam.toml(add at the end):[tailwind] version = "4.0.8" # optional, uses latest if omitted args = [ "--input=./priv/static/input.css", "--output=./priv/static/styles.css", "--watch" # optional, for auto-rebuild during development ]
-
Create
priv/static/input.css:@import "tailwindcss"; /* Add custom styles below */ @layer components { .btn-primary { @apply bg-blue-500 text-white px-4 py-2 rounded; } }
-
Build CSS (run during development):
gleam run -m tailwind/run
Or run once without watch mode by removing
"--watch"fromgleam.toml. -
Include in HTML:
<link rel="stylesheet" href="/styles.css">
Or in Lustre components:
html.link([attribute.rel("stylesheet"), attribute.href("/styles.css")])
Tip: Run gleam run -m tailwind/run in a separate terminal during development to auto-rebuild CSS on changes.
- Restart the server: Stop
gleam run(Ctrl+C) and run it again - Components are compiled into the server binary, not served dynamically
- Run
gleam check- it will show you the error - Check parentheses: Every
html.div([], [])needs two arrays: attributes and children - Check commas: Gleam uses commas between list items
- Check the path: Files in
priv/static/are served from root, e.g.,priv/static/styles.css→http://localhost:8000/styles.css - Check file extension: Only certain extensions are served (ask backend dev to check
web.gleamif unsure)
- Lustre docs: https://hexdocs.pm/lustre/
- HTMX docs: https://htmx.org/docs/
- Gleam syntax: https://tour.gleam.run/table-of-contents/
- Ask your backend dev: They can wire up new routes and help with Gleam syntax
- Explore
src/j26booking/components.gleamto see real examples - Try modifying
priv/static/index.htmland refresh the browser - Add a new component for a booking form or user profile
- Set up TailwindCSS and start styling!
This app requires you to have a postgreSQL database running locally if you want to run it.
This project uses Gleam Squirrel for type-safe database access. Squirrel generates Gleam modules from your SQL schema and queries, allowing you to interact with PostgreSQL using Gleam types and functions.
After changing or adding any SQL files in src/j26booking/sql/, regenerate the Gleam modules by running:
gleam run -m squirrelFor usage details and examples, see the official Squirrel documentation: https://hexdocs.pm/squirrel/index.html
The app for now uses a hardcoded default localhost PostgreSQL config. It requires your database to have the following connection config:
host=localhost
port=5432
user=postgres
password=
database=j26booking
Database migrations are managed using Gleam Cigogne.
You must set the DATABASE_URL environment variable with your database connection string before running migrations.
export DATABASE_URL="postgres://postgres@localhost:5432/j26booking"
gleam run -m cigogne lastThis will apply all migrations in priv/migrations/ to your database. Make sure your database is running and accessible with the config above.
To seed the database with example activities, you can run the SQL script in priv/seeding/activities.sql:
psql "$DATABASE_URL" -f priv/seeding/activities.sqlThis will insert several sample activities into the activity table. Make sure your database is running and the schema is migrated before seeding.
erDiagram
user {
uuid id PK
enum role "_organizer_, _booker_, _admin_"
}
activity {
uuid id PK
text title
text description
int[null] max_attendees
timestamp start_time
timestamp end_time
}
booking {
uuid id PK
uuid user_id FK "_booker_"
uuid activity_id FK
text group "Kår, Patrull"
text responsible "Ansvarig vuxen"
text phone_number "Till ansvarig vuxen"
int participant_count
}
activity_user {
uuid activity_id PK,FK
uuid user_id PK,FK
}
activity ||--o{ activity_user : organized_by
user ||--o{ activity_user : organizes
booking }o--|| activity : reserves
user ||--o{ booking : places
erDiagram
scout_group {
uuid id PK
text name
uuid created_by_user_id FK "_booker_"
}
user {
uuid id PK
enum role "_organizer_, _booker_, _admin_"
}
activity {
uuid id PK
text title
text description
int[null] max_attendees
timestamp start_time
timestamp end_time
}
booking {
uuid id PK
uuid user_id FK "_booker_"
uuid activity_id FK
text group "Kår, Patrull"
text responsible "Ansvarig vuxen"
text phone_number "Till ansvarig vuxen"
int participant_count
}
booking_scout_group {
uuid booking_id PK,FK
uuid scout_group_id PK,FK
}
scout_group_user {
uuid scout_group_id PK,FK
uuid user_id PK,FK
}
activity_user {
uuid activity_id PK,FK
uuid user_id PK,FK
}
booking ||--o{ booking_scout_group : includes
scout_group ||--o{ booking_scout_group : part_of
scout_group ||--o{ scout_group_user : managed_by
user ||--o{ scout_group_user : manages
activity ||--o{ activity_user : organized_by
user ||--o{ activity_user : organizes
booking }o--|| activity : reserves
user ||--o{ booking : places