Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/raczkodavid/Tikera/llms.txt

Use this file to discover all available pages before exploring further.

The frontend is a React 19 SPA built with Vite. State is managed centrally through Redux Toolkit, and all API communication flows through a single axios instance that automatically attaches the Sanctum bearer token stored in localStorage. Components are organised by feature rather than type, with clear separation between UI primitives, page-level views, and the booking workflow.

Component structure

Components live under src/components/ and are grouped into six directories:
DirectoryPurpose
pages/Top-level route components: MoviesPage, LoginPage, RegisterPage, BookingsPage, AdminPage
admin/Admin panel forms for managing movies, rooms, and screenings
booking/Booking history display and cancellation
bookingProcess/Step-by-step seat selection, ticket picker, and checkout
layout/Navbar and Footer rendered on every route
ui/Shared presentational primitives (buttons, modals, etc.)

Redux store

The store is configured in src/store/store.js using configureStore:
import { configureStore } from "@reduxjs/toolkit";
import userReducer from "./slices/userSlice";
import themeReducer from "./slices/themeSlice";
import bookingReducer from "./slices/bookingSlice";
import movieReducer from "./slices/movieSlice";

const store = configureStore({
  reducer: {
    user: userReducer,
    theme: themeReducer,
    booking: bookingReducer,
    movies: movieReducer,
  },
});

export default store;

Slices

Manages the browsing state for the movies listing page.State shape:
  • selectedWeek — ISO week number currently in view (defaults to the current week via date-fns)
  • selectedDay — ISO day of the week (1 = Monday, 7 = Sunday)
  • selectedMovie — the full movie object the user has clicked on
  • selectedScreening — the screening the user is booking
  • moviesByWeek / moviesByDay — raw and filtered movie arrays
  • loading / error — async fetch status
Async thunks:
// Fetches movies filtered by ISO week number
export const fetchMoviesByWeek = createAsyncThunk(
  "movies/fetchByWeek",
  async (weekNumber, { rejectWithValue }) => {
    try {
      return await MovieService.getMoviesByWeek(weekNumber);
    } catch (error) {
      return rejectWithValue(error.message || "Failed to fetch movies");
    }
  }
);

// Convenience thunk: fetch movies for the current week on mount
export const initializeMovies = () => async (dispatch, getState) => {
  const currentWeek = getState().movies.selectedWeek || getISOWeek(new Date());
  await dispatch(fetchMoviesByWeek(currentWeek));
};

Service layer

All API calls go through service objects exported from src/services/index.js. Each service wraps the shared api axios instance.

The api instance

// src/services/api.js
import axios from "axios";
import { toast } from "react-hot-toast";

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  headers: { "Content-Type": "application/json" },
});

// Attach token from localStorage before every request
api.interceptors.request.use((config) => {
  const token = localStorage.getItem("token");
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Translate HTTP errors to toast notifications
api.interceptors.response.use(
  (response) => response,
  (error) => {
    const message = error.response?.data?.message || "An error occurred";
    switch (error.response?.status) {
      case 400: toast.error("Bad request. Please check your input."); break;
      case 404: toast.error("Resource not found."); break;
      case 500: toast.error("Internal server error. Please try again later."); break;
      default:  toast.error(message);
    }
    return Promise.reject(error);
  }
);
The response interceptor displays a toast for every non-2xx response, so individual service functions do not need their own error UI logic.

MovieService

const MovieService = {
  getAllMovies: async () => {
    const response = await api.get("/movies");
    return response.data.data;
  },

  getMoviesByWeek: async (weekNumber) => {
    const response = await api.get(`/movies?week_number=${weekNumber}`);
    return response.data.data;
  },

  getMovie: async (id) => {
    const response = await api.get(`/movies/${id}`);
    return response.data.data;
  },

  createMovie: async (movieData) => {
    const response = await api.post("/movies", movieData);
    return response.data.data;
  },

  updateMovie: async (id, movieData) => {
    const response = await api.put(`/movies/${id}`, movieData);
    return response.data.data;
  },

  deleteMovie: async (id) => {
    await api.delete(`/movies/${id}`);
    return true;
  },
};

BookingService

const BookingService = {
  // Fetches all bookings then paginates client-side
  getUserBookings: async (page = 1, perPage = 10) => {
    const response = await api.get("/bookings");
    const allBookings = response.data.data;
    const total = allBookings.length;
    const lastPage = Math.max(1, Math.ceil(total / perPage));
    const startIndex = (page - 1) * perPage;
    return {
      data: allBookings.slice(startIndex, startIndex + perPage),
      meta: { current_page: page, last_page: lastPage, per_page: perPage, total },
    };
  },

  getBooking: async (id) => {
    const response = await api.get(`/bookings/${id}`);
    return response.data.data;
  },

  createBooking: async (bookingData) => {
    const response = await api.post("/bookings", bookingData);
    return response.data.data;
  },

  cancelBooking: async (id) => {
    await api.delete(`/bookings/${id}`);
    return true;
  },
};

AuthService

const AuthService = {
  login: async (credentials) => {
    const response = await api.post("/login", credentials);
    if (response.data.status === "success") {
      const { token, user } = response.data.data;
      localStorage.setItem("token", token);
      localStorage.setItem("userData", JSON.stringify(user));
    }
    return response.data.data;
  },

  register: async (userData) => {
    const response = await api.post("/register", userData);
    return response.data.data;
  },

  logout: () => {
    localStorage.removeItem("token");
    localStorage.removeItem("userData");
  },

  isAuthenticated: () => !!localStorage.getItem("token"),

  getCurrentUser: () => {
    const userData = localStorage.getItem("userData");
    return userData ? JSON.parse(userData) : null;
  },

  isAdmin: () => {
    const userData = localStorage.getItem("userData");
    if (!userData) return false;
    return JSON.parse(userData).role === "admin";
  },
};

Key dependencies

PackageVersionPurpose
react^19.1.0UI library
react-router-dom^7.6.0Client-side routing
@reduxjs/toolkit^2.8.1State management
react-redux^9.2.0React bindings for Redux
axios^1.9.0HTTP client
date-fns^4.1.0ISO week/day calculations
react-hot-toast^2.5.2Toast notifications
tailwindcss^4.1.6Utility-first CSS (via Vite plugin)
daisyui^5.0.35Tailwind component library (dev)
Tailwind is applied through the @tailwindcss/vite plugin rather than a PostCSS config, so there is no tailwind.config.js — all customisation lives in CSS files using @theme layers.

Build docs developers (and LLMs) love