SPACEPORT DOCS

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:

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.

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: