Skip to main content
Tripfy Africa’s frontend JavaScript has two layers: tripfy.js handles all UI enhancements and theming, while separate service worker files (serviceworker.js and firebase-messaging-sw.js) handle PWA caching and push notifications. Application assets are compiled through Vite.

tripfy.js — the main UI script

public/assets/themes/adventra/js/tripfy.js is the single JavaScript file loaded by the Adventra theme. It is an IIFE (immediately-invoked function expression) that runs with 'use strict'. It covers:

Theme toggle

Reads and writes localStorage key tf_theme. Applies data-theme to <html> before first paint to prevent flash.

Scroll reveal

Uses IntersectionObserver to add .tf-visible to .tf-reveal elements as they enter the viewport.

Animated counters

Triggers number count-up animations on stat elements via IntersectionObserver.

Checkout steps

Exports window.goToStep(n) to drive the multi-step checkout form with smooth scroll and fade transitions.

Image zoom

Clicking .open-modal[data-image] opens a full-screen overlay with the full-size image. Press Escape or click to close.

Gateway selector

Clicking a .gateway-option card selects that payment gateway and checks its underlying radio input.

Theme system

tripfy.js exposes a unified TripfyTheme global so you can interact with the theme from any inline script:
// Get the current theme
TripfyTheme.get();        // returns 'light' or 'dark'

// Set a specific theme
TripfyTheme.set('dark');
TripfyTheme.set('light');

// Toggle between light and dark
TripfyTheme.toggle();

// Convenience globals
window.tfToggleTheme();
window.tfGetTheme();      // returns 'light' or 'dark'
The toggle function also syncs with the HyperUI theme system (window.HSThemeAppearance) used by the admin and user dashboard panels. Any element with class tf-theme-toggle or attribute data-tf-theme-toggle is automatically wired as a toggle button after DOMContentLoaded.

Scroll reveal

Add the class tf-reveal to any element to opt it into the scroll-reveal animation. tripfy.js also automatically applies tf-reveal to a default set of selectors on DOMContentLoaded:
const selectors = [
  '.tour-single-card', '.destination-card', '.guide-card',
  '.review-card', '.section-title-area', '.stats-card',
  '.checkout-form-card', '.booking-sidebar', '.package-card'
];
Stagger delay is applied automatically based on element index (up to 0.35s). The header element with class .header-section receives a .scrolled class when the user scrolls more than 50px. Use this class in tripfy.css to apply a shadow or background change.

Checkout step manager

The checkout flow uses window.goToStep(n) to show and hide .checkout-panel elements and update .checkout-step progress indicators:
// Move to step 2 from an inline onclick
<button onclick="goToStep(2)">Continue</button>
Each panel animates in with a tf-fadein keyframe. After switching steps, the page scrolls to the top.

Theme mode endpoint

For authenticated users, the selected theme is persisted server-side. The route is:
GET /user/theme-mode/{themeType}
Where {themeType} is one of auto, default, or dark. The route validates the value and updates auth()->user()->theme_mode:
// routes/web.php
Route::get('theme-mode/{themeType?}', function ($themeType = 'auto') {
    $themeType = in_array($themeType, ['auto', 'default', 'dark']) ? $themeType : 'auto';
    auth()->user()->update(['theme_mode' => $themeType]);
    return response()->json(['success' => true, 'theme' => $themeType]);
})->name('theme.mode');
The admin panel has a parallel route at GET /admin/themeMode/{themeType?} that stores the preference in the session instead.

AJAX patterns

The application makes AJAX calls for several features. All POST requests include the CSRF token from <meta name="csrf-token">.
Sends the coupon code to the server and returns the discount amount. The route is POST /coupon/check (changed from GET to avoid coupon codes appearing in server logs):
fetch('/coupon/check', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
  },
  body: JSON.stringify({ coupon_code: code, package_id: packageId }),
})
.then(r => r.json())
.then(data => {
  if (data.success) applyDiscount(data.discount);
});
The messaging system uses several AJAX endpoints:
MethodRoutePurpose
POST/chat/replySend a message
GET/chat/listLoad conversation list
GET/chat/searchSearch conversations
POST/chat/detailsLoad messages for a conversation
DELETE/chat/{id}/deleteDelete a message
POST/chat/{id}/nickname-setSet a nickname for a conversation
Booking accept/reject operations use POST (changed from GET to prevent CSRF via URL):
fetch('/booking/accept', {
  method: 'POST',
  headers: { 'X-CSRF-TOKEN': csrfToken },
  body: formData,
});

Vite build setup

The project uses Vite with the Laravel Vite plugin. The entry points are resources/sass/app.scss and resources/js/app.js:
// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';

export default defineConfig({
  plugins: [
    laravel({
      input: [
        'resources/sass/app.scss',
        'resources/js/app.js',
      ],
      refresh: true,
    }),
  ],
});
Key dev dependencies from package.json:
PackageVersionPurpose
vite^4.0.0Build tool
laravel-vite-plugin^0.7.5HMR and manifest integration
bootstrap^5.2.3CSS framework
@popperjs/core^2.11.6Bootstrap dropdown dependency
sass^1.56.1SCSS compilation
axios^1.1.2HTTP client for JS
Run the dev server with:
npm run dev
Build for production with:
npm run build
The Adventra theme’s tripfy.css and tripfy.js are pre-built static assets in public/assets/themes/adventra/ and are not compiled by Vite. Vite only processes files in resources/sass/ and resources/js/.

PWA service worker

serviceworker.js (in the project root) is the PWA service worker. It uses a versioned cache name based on the current timestamp:
// serviceworker.js
var staticCacheName = "pwa-v" + new Date().getTime();
var filesToCache = [];

// Cache on install
self.addEventListener("install", event => {
  this.skipWaiting();
  event.waitUntil(
    caches.open(staticCacheName).then(cache => cache.addAll(filesToCache))
  );
});

// Serve from cache; fall back to network; fall back to 'offline' page
self.addEventListener("fetch", event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => response || fetch(event.request))
      .catch(() => caches.match('offline'))
  );
});

// Remove old caches on activate
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames =>
      Promise.all(
        cacheNames
          .filter(name => name.startsWith("pwa-") && name !== staticCacheName)
          .map(name => caches.delete(name))
      )
    )
  );
});
Registration is triggered from app.js via:
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/serviceworker.js');
}

Firebase push notifications

firebase-messaging-sw.js (in the project root) handles background push notifications via Firebase Cloud Messaging (FCM) v8:
// firebase-messaging-sw.js
importScripts('https://www.gstatic.com/firebasejs/8.3.2/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/8.3.2/firebase-messaging.js');

const firebaseConfig = {
  apiKey:            "Your_API_Key",
  authDomain:        "appSecret",
  projectId:         "Project_Id",
  storageBucket:     "Storage_Bucket",
  messagingSenderId: "Sender_Id",
  appId:             "App_Id",
  measurementId:     "Measurement_Id",
};

const app       = firebase.initializeApp(firebaseConfig);
const messaging = firebase.messaging();

// Show notification when app is in background
messaging.setBackgroundMessageHandler(function (payload) {
  if (payload.notification.background == 1) {
    return self.registration.showNotification(
      payload.notification.title,
      { body: payload.notification.body, icon: payload.notification.icon }
    );
  }
});

// Handle notification click — open the click_action URL
self.onnotificationclick = (event) => {
  if (event.notification.data.FCM_MSG.data.click_action) {
    event.notification.close();
    event.waitUntil(
      clients.openWindow(event.notification.data.FCM_MSG.data.click_action)
    );
  }
};
Before going to production, replace every placeholder value in firebaseConfig with your real Firebase project credentials. You configure Firebase credentials through Admin → Settings → Push Notification.

Build docs developers (and LLMs) love