Tutorial: Building a Museum Booking App with Next.js and Timerise API

This tutorial guides you through creating a museum booking app using Next.js, TypeScript, Timerise API, Apollo Client, and Tailwind CSS. We will focus on integrating the headless Timerise API, demonstrating the benefits of such a solution.

Prerequisites

  • Basic knowledge of TypeScript and React
  • Node.js installed
  • Access to Timerise API

Step 1: Setting Up the Next.js Project

The first step in building our museum booking app is to set up a new Next.js project. Next.js introduces an enhanced setup process, allowing you to configure essential features like Tailwind CSS, ESLint, and more, right from the start. This streamlined approach simplifies initial project setup, letting us focus on building the app’s core functionalities.

Create a New Next.js App
Run the command to initiate the creation of a new Next.js app. You’ll be prompted to choose various configuration options, including the integration of Tailwind CSS, which we’ll be using for styling our app.

Bash
npx create-next-app@latest museum-booking-app --typescript
cd museum-booking-app

When prompted, choose your preferred configurations:

  • ESLint: Choose ‘Yes‘ for code quality checks.
  • Tailwind CSS: Select ‘Yes‘ to set up Tailwind CSS automatically.
  • Use src/ directory: Choose ‘Yes‘ for a cleaner project structure.
  • App Router: Opt for ‘Yes‘ to use the new App Router feature.
  • Customize Default Import Alias: Select ‘Yes‘ if you want to customize import paths.

Step 2: Installing Dependencies

With the enhanced Next.js setup, key dependencies like Tailwind CSS are already integrated. However, we still need to install additional packages that are crucial for our application, particularly Apollo Client for handling GraphQL queries and mutations with the Timerise API.

Install Apollo Client
Apollo Client is a comprehensive state management library for JavaScript that enables you to manage both local and remote data with GraphQL. This will be our primary tool for interacting with the Timerise API.

Bash
npm install @apollo/client graphql

Verify Tailwind CSS Configuration (Optional)
Since Tailwind CSS has been set up during the project initialization, verify the configuration files (tailwind.config.js and postcss.config.js) to ensure they meet your project’s styling needs. You can make adjustments as necessary to customize the Tailwind setup.

Additional Dependencies
Depending on your project requirements, you may need other dependencies. For example, if you plan to handle dates and times (common in a booking app), consider installing date-fns:

Bash
npm install date-fns

Step 3: Configuring Apollo Client

Now that our project is set up with the necessary dependencies, it’s time to configure Apollo Client. Apollo Client is a powerful and flexible tool for managing both local and remote data in JavaScript applications. It simplifies the process of interacting with GraphQL APIs, like the Timerise API we are using. By setting up Apollo Client, we enable our application to efficiently execute queries and mutations, manage loading states, cache data, and handle errors, which are essential aspects of interacting with our booking system.

Set Up Apollo Client in lib/apolloClient.ts

We need to create an instance of Apollo Client that points to our GraphQL API endpoint. This instance will be used throughout the application to interact with the Timerise API.

TypeScript
import { ApolloClient, InMemoryCache } from '@apollo/client';

const apolloClient = new ApolloClient({
  uri: 'https://sandbox-api.timerise.io/v1',
  cache: new InMemoryCache(),
});

export default apolloClient;
  • The uri property is set to the endpoint of the Timerise API.
  • InMemoryCache is used to cache query results after fetching them. This helps in optimizing the performance of our application by reducing the number of API calls needed.

This configuration is a crucial step in enabling our application to communicate effectively with the Timerise API. It sets the stage for the next steps where we will be creating and executing GraphQL queries and mutations to handle bookings.

Wrap Your Application with ApolloProvider
In your app/page.tsx file, import ApolloProvider from @apollo/client and your Apollo Client instance. Then, wrap your application’s root component with and pass the client instance to it.

TypeScript
'use client'
import { ApolloProvider } from '@apollo/client';
import apolloClient from '../lib/apolloClient';
import SlotSelection from "@/components/SlotSelection";

export default function Home() {
  return (
    <ApolloProvider client={apolloClient}>
      <SlotSelection serviceId="YourServiceId" />
    </ApolloProvider>
  );
};

Step 4: Creating GraphQL Queries and Mutations

GraphQL queries allow us to fetch data, and mutations let us modify data. Here, we define the GraphQL queries and mutations needed to interact with the Timerise API for fetching services and booking slots.

Define GraphQL Operations in graphql/queries.ts

TypeScript
import { gql } from '@apollo/client';

export const SERVICE_QUERY = gql`
  query Service($serviceId: ID!, $slotType: SlotType!) {
    service(serviceId: $serviceId) {
      serviceId
      title
      description
      media {
        title
        url
      }
      slots(slotType: $slotType) {
        slotId
        dateTimeFrom
        dateTimeTo
        duration
        quantity
      }
    }
  }
`;

export const BOOKING_QUERY = gql`
  query Booking($bookingId: ID!) {
    booking(bookingId: $bookingId) {
      bookingId
      shortId
      shortUrl
      qrUrl
      status
    }
  }
`;

Define Mutation in graphql/mutations.ts

TypeScript
import { gql } from '@apollo/client';

export const BOOKING_CREATE_MUTATION = gql`
  mutation BookingCreate($serviceId: ID!, $slots: [ID]) {
    bookingCreate(serviceId: $serviceId, slots: $slots) {
      bookingId
      qrUrl
      shortUrl
      status
    }
  }
`;

Step 5: Building the Application Components

With our API interactions defined, we now focus on building the React components. These components will provide the interface for selecting slots and confirming bookings, utilizing the queries and mutations we’ve set up.

Slot Selection Component (SlotSelection.tsx)
Code and explanation for creating a component to select available slots.

TypeScript
import React, { useState } from 'react';
import { useRouter } from 'next/navigation'
import { useQuery, useMutation } from '@apollo/client';
import { SERVICE_QUERY } from '../graphql/queries';
import { BOOKING_CREATE_MUTATION } from '../graphql/mutations';

const SlotSelection = ({ serviceId }: { serviceId: string }) => {
  const router = useRouter();
  const [selectedSlot, setSelectedSlot] = useState<string|null>(null);
  const { loading, error, data } = useQuery(SERVICE_QUERY, {
    variables: { serviceId: serviceId, slotType: "AVAILABLE" },
  });
  const [createBooking, { data: bookingData, loading: bookingLoading, error: bookingError }] = useMutation(BOOKING_CREATE_MUTATION);

  if (loading || bookingLoading) return <p>Loading...</p>;
  if (error || bookingError) return <p>Error loading.</p>;

  const handleSlotSelect = async (slotId: string) => {
    setSelectedSlot(slotId);
    try {
      const { data } = await createBooking({
        variables: { serviceId, slots: [slotId] }
      });
      if (data.bookingCreate && data.bookingCreate.bookingId) {
        router.push(`/confirmation/${data.bookingCreate.bookingId}`);
      }
    } catch (error) {
      console.error("Error creating booking:", error);
      // Handle booking error
    }
  };

  return (
    <div className="p-4">
      <h2 className="text-lg font-semibold">Select a Slot</h2>
      <ul>
        {data.service.slots.map((slot: { slotId: string, dateTimeFrom: Date }) => (
          <li key={slot.slotId} className="my-2">
            <button
              className={`p-2 border ${selectedSlot === slot.slotId ? 'border-blue-500' : 'border-gray-300'}`}
              onClick={() => handleSlotSelect(slot.slotId)}>
              {new Date(slot.dateTimeFrom).toLocaleString()}
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default SlotSelection;

Confirmation Component (ConfirmationView.tsx)
Code and explanation for creating a component that displays booking confirmation details, including the QR code.

TypeScript
import React from 'react';
import { useQuery } from '@apollo/client';
import { BOOKING_QUERY } from '@/graphql/queries';

const ConfirmationView = ({ bookingId }: { bookingId: string }) => {
  const { loading, error, data } = useQuery(BOOKING_QUERY, {
    variables: { bookingId: bookingId },
  });

  if (loading) return <p>Loading booking...</p>;
  if (error) return <p>Error loading booking.</p>;
  return (
    <div className="p-4">
      <h2 className="text-lg font-semibold">Booking status: {data.booking.status}</h2>
      <p className="my-2">Booking ID: {data.booking.shortId}</p>
      <div className="my-2">
        <img src={data.booking.qrUrl} alt="QR Code" className="w-32 h-32" />
      </div>
      <a href={data.booking.shortUrl} target="_blank" className="text-blue-500">View booking page</a>
    </div>
  );
};

export default ConfirmationView;

Step 6: Front-End Development with Tailwind CSS

This step is all about styling our application. Tailwind CSS, a utility-first CSS framework, will be used for designing a clean and responsive user interface. This approach allows us to build a visually appealing front-end quickly and efficiently, enhancing the user experience.

In SlotSelection.tsx, we’ll apply Tailwind classes for layout, spacing, and aesthetics.

TypeScript
return (
  <div className="p-4 max-w-md mx-auto">
    <h2 className="text-2xl font-bold text-center text-white-800 mb-4">Select a Slot</h2>
    <ul className="list-none space-y-3">
        {data.service.slots.map((slot: { slotId: string, dateTimeFrom: Date }) => (
          <li key={slot.slotId} className="flex justify-between items-center p-3 border rounded-lg shadow-sm">
            <span className="text-white-600">{new Date(slot.dateTimeFrom).toLocaleString()}</span>
            <button
              className={`px-4 py-2 rounded-lg text-black ${selectedSlot === slot.slotId ? 'bg-blue-500' : 'bg-gray-300'}`}
              onClick={() => handleSlotSelect(slot.slotId)}>
              Select
            </button>
          </li>
        ))}
    </ul>
  </div>
);

In ConfirmationView.tsx, style the booking confirmation details.

TypeScript
return (
  <div className="p-4 max-w-md mx-auto">
    <h2 className="text-2xl font-bold text-center text-white-800 mb-4">BOOKING {data.booking.status}</h2>
    <div className="bg-white shadow overflow-hidden sm:rounded-lg p-4">
      <p className="text-center text-gray-600 mb-2">ID: <span className="text-gray-800 font-semibold">{data.booking.shortId}</span></p>
      <div className="text-center">
        <img src={data.booking.qrUrl} alt="QR Code" className="w-32 h-32 inline-block mb-3" />
        <p className="text-sm text-gray-600">Scan this QR code at the entrance</p>
      </div>
      <a href={data.booking.shortUrl} target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:text-blue-700 text-center block mt-4">
        View booking page
      </a>
    </div>
  </div>
);

Step 7: Deploying with Vercel

Deploying our app makes it accessible to users on the web. Vercel, harmonizing well with Next.js, offers a seamless deployment experience. It automates the process from code push to production, handling serverless functions, static file serving, and more, ensuring optimal performance and scalability.

Push Your Code to GitHub
Before deploying, ensure your project is pushed to a GitHub repository:

Bash
git init
git add .
git commit -m "Init commit"
git branch -M main
git remote add origin https://github.com/your-username/your-repo-name.git
git push -u origin main

Set Up Deployment on Vercel

  1. Sign in to Vercel and connect your GitHub account.
  2. Choose the repository you just pushed.
  3. Vercel automatically detects it’s a Next.js app and suggests build settings. Accept them or modify as needed.
  4. Click ‘Deploy‘ to start the deployment process. Vercel handles the build and deployment, providing you with a live URL.

Vercel provides features like automatic HTTPS, custom domain support, and real-time analytics. You can also set environment variables and manage other settings directly from the Vercel dashboard.

Accessing the Complete Application Code

To complement this tutorial and enhance your learning experience, we’ve made the complete source code of the museum booking application available on GitHub. This is a great opportunity for you to explore the codebase, experiment with modifications, and understand the application’s structure and functionality in greater detail.

Access the Repository
The entire source code for the museum booking application is hosted on GitHub. You can access it at the following URL: https://github.com/timerise-io/museum-booking-app

Clone the Repository
To work with the code on your local machine, clone the repository using Git:

Bash
git clone https://github.com/timerise-io/museum-booking-app.git
cd museum-booking-app

Explore and Modify
Once you have the code, feel free to explore and modify it. Whether you want to understand specific functionalities, add new features, or customize the UI, this codebase can serve as a practical starting point.

Visit the Demo
You can access the live demo of the Museum Booking App at the following URL: https://museum-booking-app.vercel.app/

Conclusion

This tutorial provided a step-by-step guide on setting up a Next.js application, integrating with a headless API like Timerise, and deploying it. The code snippets are crucial parts of the application, demonstrating how to integrate the Timerise API with a Next.js app using Apollo Client for GraphQL.