Your First Application: Meeting Room Booking
Welcome to Spaceport! This tutorial is designed to be your first hands-on experience, guiding you through the creation of a simple but fully functional meeting room booking system. It's the perfect way to see Spaceport's core features in action without the commitment of a full scaffold.
By the end of this guide, you will have learned the basics of:
- Minimal Scaffolding: Setting up a project with just a couple of folders.
- Source Modules: Writing your application's logic in a Groovy file.
- State Management: Storing the booking schedule on the server.
- Launchpad Templates: Creating the user interface with
.ghtmlfiles. - Server Actions & Transmissions: Making the UI interactive by connecting button clicks directly to your server-side Groovy code.
We'll assume you have already completed the Developer Onboarding Guide and have Java and CouchDB installed and running.
If you're curious what this project looks like when it's finished, you can check out the Meeting Room Booker Live Demo. Note: This demo is slightly enhanced to allow for multiple simultaneous booking instances bound to the client's docking session, an enhancement that is part of the "Taking It Further" section at the end of this tutorial.
# Step 1: Create the Project Structure
First, let's create a minimal project scaffold. All you need is a main folder for your project and a couple of subdirectories.
- Create a new folder for your project (e.g.,
my-booking-app). - Inside that folder, create two subdirectories:
modulesandlaunchpad. - Inside
launchpad, create one more subdirectory namedparts.
Your scaffolding structure should look like this:
+ /my-booking-app
+ modules/
+ launchpad/
+ parts/
+ elements/
Next, download the latest Spaceport JAR file into your main project folder.
curl -L https://spaceport.com.co/builds/spaceport-latest.jar -o spaceport.jar
Your near-complete project scaffold should now look like this:
+ /my-booking-app
- spaceport.jar
+ modules/
+ launchpad/
+ parts/
+ elements/
# Step 2: Write the Booking Logic & Router
All of our server-side logic will live in a single Source Module. This file will manage the booking schedule, handle reservations, and tell Spaceport how to serve our application to the browser.
Create a new file named Booking.groovy inside your modules/ folder and let's build it piece by piece.
## Part A: Booking State
First, define the class and the variables that will hold our booking state. We use static variables so they are shared
across all requests and act as a simple, in-memory "database" for our schedule.
import spaceport.computer.alerts.Alert
import spaceport.computer.alerts.results.*
import spaceport.launchpad.Launchpad
class Booking {
//
// Booking State
static List<String> schedule // A list of 9 strings representing time slots ('Conference A', 'Conference B', or '')
static String currentRoom // Which room the user has selected to book, 'Conference A' or 'Conference B'
// ... more code will go here
}
## Part B: Core Booking Logic
Next, add the methods that control the booking system. These methods will reset the schedule, process a booking, toggle the selected room, and check if the schedule is fully booked.
// ... inside the Booking class
//
// Core Booking Logic
// Sets the schedule back to its starting state
static void resetSchedule() {
schedule = (0..8).collect { '' } // A list of 9 empty strings
currentRoom = 'Conference A'
}
// Handles a room booking
static void bookSlot(int index) {
// Only allow bookings if the slot is empty
if (schedule[index] == '') {
// Book the current room for this time slot
schedule[index] = currentRoom
}
}
// Toggles between Conference A and Conference B
static void toggleRoom() {
currentRoom = (currentRoom == 'Conference A' ? 'Conference B' : 'Conference A')
}
// Checks if all slots are booked
static boolean isFullyBooked() {
return !schedule.contains('')
}
## Part C: Spaceport Hooks & Router
Finally, we need to connect our logic to Spaceport. We'll use Alerts to hook into Spaceport's event system. One alert will initialize the schedule when the server starts, and another will act as a router to show the booking interface in the browser.
// ... inside the Booking class
//
// Spaceport Alerts (Hooks) & Router
static def launchpad = new Launchpad() // Create a Launchpad instance
// This Alert runs once when the application starts.
@Alert('on initialize')
static _init(Result r) {
resetSchedule() // Initialize the booking schedule
}
// This Alert tells Spaceport what to do when someone visits our website's root URL ("/").
@Alert('on / hit')
static _renderBooking(HttpResult r) {
// This command tells Launchpad to render the 'index.ghtml' template with the context from 'r'.
launchpad.assemble(['index.ghtml']).launch(r)
}
## Completed Booking.groovy File
When you're done, your modules/Booking.groovy file should look like this:
import spaceport.computer.alerts.Alert
import spaceport.computer.alerts.results.*
import spaceport.launchpad.Launchpad
class Booking {
//
// Booking State
static List<String> schedule
static String currentRoom
//
// Core Booking Logic
static void resetSchedule() {
schedule = (0..8).collect { '' }
currentRoom = 'Conference A'
}
static void bookSlot(int index) {
if (schedule[index] == '') {
schedule[index] = currentRoom
}
}
static void toggleRoom() {
currentRoom = (currentRoom == 'Conference A' ? 'Conference B' : 'Conference A')
}
static boolean isFullyBooked() {
return !schedule.contains('')
}
//
// Spaceport Alerts (Hooks) & Router
static def launchpad = new Launchpad()
@Alert('on initialize')
static _init(Result r) {
resetSchedule()
}
@Alert('on / hit')
static _renderBooking(HttpResult r) {
launchpad.assemble(['index.ghtml']).launch(r)
}
}
With the newly created Source Module, your project scaffold looks like this:
+ /my-booking-app
- spaceport.jar
+ modules/
- Booking.groovy
+ launchpad/
+ parts/
+ elements/
# Step 3: Create the User Interface
Now, let's create the visual part of our booking system using a Launchpad template. This .ghtml file will
contain our HTML, CSS, and some special Spaceport attributes to make the booking interface interactive.
Create a new file named index.ghtml inside your launchpad/parts/ folder.
## Part A: HTML Boilerplate and Styling
Start with the basic HTML structure. We need to include the HUD-Core.js script, which is essential for enabling
Launchpad's interactive features like Server Actions and Transmissions. For simplicity, we'll embed our CSS directly
in a <style> tag.
Launchpad has the idea of a Vessel, which can act like a wrapper for your entire page, letting you abstract some of the boilerplate away. However, for this simple example, we'll just use a single file.
<!DOCTYPE html>
<html>
<head>
<title>Spaceport Meeting Room Booking</title>
<script defer src='https://cdn.jsdelivr.net/gh/spaceport-dev/hud-core.js@latest/hud-core.min.js'></script>
<style>
@view-transition { navigation: auto; }
html { font-size: 14px; }
body { user-select: none; font-family: sans-serif; display: grid; place-content: center; text-align: center; background-color: #1a1a2e; color: white; gap: 40px; padding: 40px; }
.schedule { display: grid; grid-template-columns: repeat(3, 100px); gap: 5px; background-color: white; border: 5px solid white; border-radius: 18px; }
.slot { aspect-ratio: 5/3; background-color: #1a1a2e; font-size: 0.9em; display: grid; place-content: center; cursor: pointer; border-radius: 10px; padding: 10px; }
.slot:hover { outline: 5px solid #4a90e2; }
.status { background-color: white; color: #1a1a2e; border-radius: 10px; font-size: 1.25em; min-height: 50px; display: grid; place-content: center; padding: 20px; }
button { background-color: #1a1a2e; font-size: 1em; padding: 10px 20px; cursor: pointer; color: white; border: 5px solid white; border-radius: 10px; }
button:hover { outline: 5px solid #4a90e2; }
.toggle-btn { background-color: #4a90e2; border-color: #4a90e2; }
.toggle-btn:hover { outline: 5px solid white; }
.legend { display: flex; gap: 20px; justify-content: center; font-size: 0.9em; }
.legend-item { display: flex; align-items: center; gap: 8px; }
.legend-box { width: 20px; height: 20px; border-radius: 4px; }
.conf-a { background-color: #e74c3c; }
.conf-b { background-color: #3498db; }
</style>
</head>
<body>
<h1>Spaceport <br> Meeting Room Booking</h1>
/// ... we'll add more code here in the next section
</body>
</html>
Note: You'll notice /// (triple slashes) in the code above. Inside Launchpad Templates, these are used to denote server comments which can be used inside your server-side Groovy code blocks, HTML, CSS, and JavaScript. They are ignored by the server and do not appear in the rendered HTML.
## Part B: Displaying Dynamic Content
Inside the <body>, we'll add the UI. First, a status section that shows which room is currently selected for booking,
and whether the schedule is fully booked. We can embed Groovy code directly in our HTML using <% ... %> scriptlets to
access the static variables from our Booking.groovy class.
/// ... inside the <body> tag, after the <h1> tag
<div class="status">
/// Use Groovy code to display the current booking status
<% if (Booking.isFullyBooked()) { %>
/// Show a message when all slots are booked
All time slots are fully booked!
<% } else { %>
/// If there's still availability, show which room is selected
Currently booking: ${ Booking.currentRoom }
<% } %>
</div>
<button class="toggle-btn" on-click="${ _{
Booking.toggleRoom()
return [ '@redirect' : '/' ]
}}">
Switch to ${ Booking.currentRoom == 'Conference A' ? 'Conference B' : 'Conference A' }
</button>
## Part C: Rendering an Interactive Schedule
Next, we'll render the booking schedule by looping through our Booking.schedule list. The interactive magic happens in the on-click
attribute. This is a Server Action—one of Launchpad's core features. When a user clicks a slot, the Groovy code inside ${ _{ ... } } is executed
on the server. It calls our bookSlot() method and then returns a Transmission, [ '@redirect' : '/' ], which
tells the browser to reload the page and show the updated schedule. This could also be a @reload, but we've included
a standard view transition for a neat fade effect between bookings which requires a page navigation, which @redirect provides.
/// ...inside the <body> tag, after the status <div>
<div class="schedule">
/// Loop through the schedule from our Booking.groovy file
<% Booking.schedule.eachWithIndex { slot, i -> %>
<div class="slot ${ 'conf-a'.if { slot == 'Conference A' }} ${ 'conf-b'.if { slot == 'Conference B' }}"
on-click="${ _{
Booking.bookSlot(i)
return [ '@redirect' : '/' ]
}}">
${ slot ?: "Slot ${i+1}" }
</div>
<% } %>
</div>
## Part D: The Legend and Reset Button
Finally, add a legend to show which color represents which room, and a reset button that uses the same Server Action pattern
to call Booking.resetSchedule() and reload the page.
/// ... inside the <body> tag, after the schedule <div>
<div class="legend">
<div class="legend-item">
<div class="legend-box conf-a"></div>
<span>Conference A</span>
</div>
<div class="legend-item">
<div class="legend-box conf-b"></div>
<span>Conference B</span>
</div>
</div>
<button on-click="${ _{
Booking.resetSchedule()
return [ '@redirect' : '/' ]
}}">
Clear Schedule
</button>
## Completed index.ghtml File
Your final launchpad/parts/index.ghtml file should look like this:
<!DOCTYPE html>
<html>
<head>
<title>Spaceport Meeting Room Booking</title>
<script defer src='https://cdn.jsdelivr.net/gh/spaceport-dev/hud-core.js@latest/hud-core.min.js'></script>
<style>
@view-transition { navigation: auto; }
html { font-size: 14px; }
body { user-select: none; font-family: sans-serif; display: grid; place-content: center; text-align: center; background-color: #1a1a2e; color: white; gap: 40px; padding: 40px; }
.schedule { display: grid; grid-template-columns: repeat(3, 100px); gap: 5px; background-color: white; border: 5px solid white; border-radius: 18px; }
.slot { aspect-ratio: 5/3; background-color: #1a1a2e; font-size: 0.9em; display: grid; place-content: center; cursor: pointer; border-radius: 10px; padding: 10px; }
.slot:hover { outline: 5px solid #4a90e2; }
.status { background-color: white; color: #1a1a2e; border-radius: 10px; font-size: 1.25em; min-height: 50px; display: grid; place-content: center; padding: 20px; }
button { background-color: #1a1a2e; font-size: 1em; padding: 10px 20px; cursor: pointer; color: white; border: 5px solid white; border-radius: 10px; }
button:hover { outline: 5px solid #4a90e2; }
.toggle-btn { background-color: #4a90e2; border-color: #4a90e2; }
.toggle-btn:hover { outline: 5px solid white; }
.legend { display: flex; gap: 20px; justify-content: center; font-size: 0.9em; }
.legend-item { display: flex; align-items: center; gap: 8px; }
.legend-box { width: 20px; height: 20px; border-radius: 4px; }
.conf-a { background-color: #e74c3c; }
.conf-b { background-color: #3498db; }
</style>
</head>
<body>
<h1>Spaceport <br> Meeting Room Booking</h1>
<div class="status">
/// Use Groovy code to display the current booking status
<% if (Booking.isFullyBooked()) { %>
/// Show a message when all slots are booked
All time slots are fully booked!
<% } else { %>
/// If there's still availability, show which room is selected
Currently booking: ${ Booking.currentRoom }
<% } %>
</div>
<button class="toggle-btn" on-click="${ _{
Booking.toggleRoom()
return [ '@redirect' : '/' ]
}}">
Switch to ${ Booking.currentRoom == 'Conference A' ? 'Conference B' : 'Conference A' }
</button>
<div class="schedule">
/// Loop through the schedule from our Booking.groovy file
<% Booking.schedule.eachWithIndex { slot, i -> %>
<div class="slot ${ 'conf-a'.if { slot == 'Conference A' }} ${ 'conf-b'.if { slot == 'Conference B' }}"
on-click="${ _{
Booking.bookSlot(i)
return [ '@redirect' : '/' ]
}}">
${ slot ?: "Slot ${i+1}" }
</div>
<% } %>
</div>
<div class="legend">
<div class="legend-item">
<div class="legend-box conf-a"></div>
<span>Conference A</span>
</div>
<div class="legend-item">
<div class="legend-box conf-b"></div>
<span>Conference B</span>
</div>
</div>
<button on-click="${ _{
Booking.resetSchedule()
return [ '@redirect' : '/' ]
}}">
Clear Schedule
</button>
</body>
</html>
With all files in place, your complete meeting room booking project scaffold looks like this:
+ /my-booking-app
- spaceport.jar
+ modules/
- Booking.groovy
+ launchpad/
+ parts/
- index.ghtml
+ elements/
# Step 4: Launch and Book!
That's it! You've written all the code needed for a complete, interactive web application.
To run your booking system, open your terminal, navigate to your project's root folder (my-booking-app), and run the
following command:
java -jar spaceport.jar --start --no-manifest
Spaceport will start up. Now, open your web browser and go to: http://localhost:10000
You should see your meeting room booking interface. Click on the time slots to book them for Conference A or Conference B, and use the Clear Schedule button to reset all bookings.
# Taking It Further
Congratulations on building your first Spaceport application! You've just created a complete, interactive experience using a classic Multi-Page Application (MPA) pattern. It's a bit ironic to call it "multi-page" when there's only one view, but the term describes the technique: the server generates a completely new HTML document in response to each user action, which is a powerful and simple way to manage server-driven state.
This is a great starting point, but the true philosophy of Spaceport is a hybrid approach. It's designed to blend the simplicity of an MPA with the fluid, app-like experience of a Single-Page Application (SPA). A SPA avoids full page reloads by dynamically rewriting parts of the existing page with new data from the server. While SPAs and MPAs are poles apart in terms of architecture, Spaceport makes it easy to start with an MPA and gradually add SPA-like features when needed.
When you enhance an MPA with features like Spaceport's Launchpad Transmissions, it starts to exhibit the best qualities of a SPA. You get a smooth user experience without the complexity of a full client-side framework.
## Evolve Your Application
Here are a few ways to take your booking system to the next level:
- Implement Real-Time Updates (SPA-like qualities) Instead of reloading the entire page, use a targeted Launchpad Transmission to update only the clicked slot and the status message. The server will respond with tiny instructions to manipulate the existing page, making the booking feel instantaneous. This is the first and most important step in shifting from a pure MPA to a hybrid model. There's a lot to learn with Launchpad and Transmissions, but it's worth the effort.
- Externalize Your Styles Improve your project's organization by moving the CSS into a dedicated
.cssfile and serving it using Spaceport's static asset handling. This allows the browser to cache your styles, improving performance. Spaceport's Static Assets documentation will guide you through the setup. This is also a great time to implement a Spaceport Manifest Configuration file to manage your app's scaffold and settings.
- Support Multiple Users Currently, there's only one 'view' for everyone on the server, and no additional information about who booked what. Learning about Sessions & Client Management would provide a great starting point for dealing with multiple users, and allowing each user to have their own booking metadata (e.g., team name, user ID, etc.).
- Add Time Slot Label and Configuration Replace the generic "Slot 1, Slot 2..." labels with actual time ranges (9:00 AM - 10:00 AM, etc.) to make the booking interface more realistic and useful for real-world scheduling scenarios. Better yet, allow a super user to configure the time slots via a simple admin UI using additional Routing, and additional state management with Documents or Cargo.
SPACEPORT DOCS