Rabu, 08 April 2026

Gemini - Membuat Website Travel

 





PROMPT

Prompt: Membangun Aplikasi Landing Page Travel (SPA) dengan Google Apps Script

Peran: Bertindaklah sebagai Senior Full-stack Web Developer.

Tujuan: Buatlah sebuah aplikasi web satu halaman (Single Page Application - SPA) untuk layanan "Info Travel" khusus destinasi Labuan Bajo. Aplikasi ini harus mengintegrasikan formulir Lead Generation dengan Google Sheets sebagai database melalui Google Apps Script.

1. Struktur Backend (Code.gs)

Buatlah logika server-side yang mencakup:

  • doGet(): Fungsi untuk menyajikan file index.html.

  • setupDatabase(): Fungsi inisialisasi untuk membuat header kolom (Nama, Email, WhatsApp, Timestamp) di spreadsheet jika masih kosong.

  • saveLead(formData): Fungsi untuk menyimpan isian formulir ke baris baru.

  • getLeads(): Fungsi untuk mengambil data dari spreadsheet. Penting: Konversi semua objek Date menjadi string ISO (toISOString) agar data bisa dikirim ke frontend tanpa error serialisasi.

2. Struktur Frontend (index.html)

Gunakan Tailwind CSS untuk gaya dan bangun antarmuka dengan struktur SPA (dua bagian utama: Home dan Dashboard Admin):

A. Bagian Home (Landing Page):

  • Hero Section: Headline "Jelajahi Surga Tersembunyi Labuan Bajo Tanpa Ribet Logistik". Sub-headline tentang layanan premium Live on Board. Latar belakang gambar cinematic kapal Phinisi.

  • Lead Form: Formulir di area Hero (Nama, Email, WhatsApp). Tombol CTA kontras berwarna Sunset Orange (#fb8500).

  • Benefits: Jelaskan layanan "All-in Service" dan "Professional Documentation".

  • Palet Warna: Dominasi Ocean Blue (#0077b6) dan Sunset Orange (#fb8500).

B. Bagian Dashboard Admin:

  • Tabel data responsif untuk menampilkan: Waktu Masuk, Nama, Email, dan WhatsApp (dengan link otomatis ke wa.me).

  • Tombol "Refresh Data" yang memicu pengambilan data terbaru.

3. Logika Komunikasi Data

  • Gunakan google.script.run untuk mengirim data dari formulir ke fungsi saveLead.

  • Gunakan google.script.run dengan withSuccessHandler untuk mengambil data melalui getLeads dan menampilkannya di tabel dashboard.

  • Implementasikan navigasi SPA sederhana berbasis JavaScript (fungsi MapsTo) untuk berpindah antar seksi tanpa reload halaman.

4. Fitur Tambahan & UX

  • Tambahkan Loading Spinner saat data sedang dikirim atau diambil.

  • Implementasikan Modal Success setelah formulir berhasil dikirim.

  • Desain harus Mobile-First dan memiliki whitespace yang cukup (margin antar section minimal 80px).

5. Output yang Diharapkan

Berikan dua file terpisah:

  1. Code.gs: Kode backend Google Apps Script.

  2. index.html: Kode frontend lengkap (HTML, Tailwind CSS, JS).

Code.gs

/**
 * Main function to serve index.html when the Web App is accessed.
 */
function doGet() {
  return HtmlService.createHtmlOutputFromFile('index')
      .setTitle('Info Travel - Labuan Bajo Admin')
      .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
      .addMetaTag('viewport', 'width=device-width, initial-scale=1');
}

/**
 * Initialization function to set up Google Sheets headers.
 * Run this once via the Apps Script editor.
 */
function setupDatabase() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheets()[0];
  const headers = ['Nama', 'Email', 'WhatsApp', 'Timestamp'];
 
  if (sheet.getLastRow() === 0) {
    sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
    sheet.setFrozenRows(1);
  }
  return "Database successfully prepared!";
}

/**
 * Saves new lead data from the form to Google Sheets.
 */
function saveLead(formData) {
  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = ss.getSheets()[0];
   
    const newRow = [
      formData.Nama,
      formData.Email,
      formData.WhatsApp,
      new Date() // Current Timestamp
    ];
   
    sheet.appendRow(newRow);
    return { status: 'success' };
  } catch (err) {
    throw new Error("Failed to save data: " + err.toString());
  }
}

/**
 * Retrieves all lead data for the Admin Dashboard.
 * Includes a fix for Date serialization issues.
 */
function getLeads() {
  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = ss.getSheets()[0];
    const data = sheet.getDataRange().getValues();
   
    if (data.length <= 1) return [];
   
    const headers = data[0];
    const rows = data.slice(1).map(row => {
      let obj = {};
      headers.forEach((header, i) => {
        let value = row[i];
       
        // CRITICAL FIX: Convert Date objects to strings (ISO format)
        // google.script.run cannot pass raw Date objects back to the client.
        if (value instanceof Date) {
          value = value.toISOString();
        }
       
        obj[header] = value;
      });
      return obj;
    });
   
    return rows;
  } catch (err) {
    throw new Error("Failed to fetch leads: " + err.toString());
  }
}

index.html

<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Info Travel - Labuan Bajo Eksklusif</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
    <style>
        body { font-family: 'Inter', sans-serif; scroll-behavior: smooth; }
        .color-ocean { color: #0077b6; }
        .bg-ocean { background-color: #0077b6; }
        .color-sunset { color: #fb8500; }
        .bg-sunset { background-color: #fb8500; }
       
        .hero-gradient {
            background: linear-gradient(rgba(0, 119, 182, 0.5), rgba(0, 0, 0, 0.8)),
                        url('https://images.unsplash.com/photo-1518156677180-95a2893f3e9f?auto=format&fit=crop&w=1920&q=80');
            background-size: cover;
            background-position: center;
        }

        /* SPA Section Handling */
        .page-section { display: none; }
        .page-active { display: block; }

        .loader {
            border: 2px solid #f3f3f3;
            border-top: 2px solid #fb8500;
            border-radius: 50%;
            width: 20px;
            height: 20px;
            animation: spin 1s linear infinite;
            display: inline-block;
        }
        @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
    </style>
</head>
<body class="bg-gray-50">

    <!-- Navbar SPA -->
    <nav class="fixed w-full z-50 bg-white/90 backdrop-blur-md shadow-sm">
        <div class="container mx-auto px-6 py-4 flex justify-between items-center">
            <div class="text-2xl font-bold color-ocean cursor-pointer" onclick="navigateTo('home')">
                INFO<span class="color-sunset">TRAVEL</span>
            </div>
            <div class="hidden md:flex items-center gap-8">
                <button onclick="navigateTo('home')" class="font-semibold text-gray-600 hover:color-ocean transition">Beranda</button>
                <button onclick="navigateTo('dashboard')" class="font-semibold text-gray-600 hover:color-ocean transition">Admin Dashboard</button>
                <a href="#form-cta" class="bg-sunset text-white px-6 py-2 rounded-full font-bold hover:brightness-110 transition">Pesan Sekarang</a>
            </div>
            <button class="md:hidden text-gray-600">
                <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7" />
                </svg>
            </button>
        </div>
    </nav>

    <!-- SECTION: HOME (LANDING PAGE) -->
    <main id="section-home" class="page-section page-active">
        <section class="relative min-h-screen flex items-center pt-20 hero-gradient">
            <div class="container mx-auto px-6 py-12 lg:py-24">
                <div class="flex flex-col lg:flex-row items-center gap-12">
                    <div class="lg:w-2/3 text-white">
                        <h1 class="text-4xl md:text-6xl font-bold leading-tight mb-6">
                            Jelajahi Surga Tersembunyi <span class="color-sunset">Labuan Bajo</span> Tanpa Ribet.
                        </h1>
                        <p class="text-lg md:text-xl opacity-90 mb-8 max-w-2xl text-gray-200">
                            Nikmati pengalaman Live on Board premium dengan fotografer profesional dan layanan bintang lima dalam satu paket perjalanan.
                        </p>
                    </div>

                    <div id="form-cta" class="lg:w-1/3 w-full bg-white rounded-2xl shadow-2xl p-8">
                        <h3 class="text-xl font-bold mb-2 text-gray-800">Dapatkan Itinerary & Harga</h3>
                        <p class="text-sm text-gray-600 mb-6">Brosur lengkap akan dikirim otomatis ke WhatsApp Anda.</p>
                       
                        <form id="travelForm" class="space-y-4">
                            <input type="text" name="Nama" id="inputNama" placeholder="Nama Lengkap" class="w-full px-4 py-3 rounded-lg border focus:ring-2 focus:ring-blue-500 outline-none" required>
                            <input type="email" name="Email" id="inputEmail" placeholder="Alamat Email" class="w-full px-4 py-3 rounded-lg border focus:ring-2 focus:ring-blue-500 outline-none" required>
                            <input type="tel" name="WhatsApp" id="inputWA" placeholder="No. WhatsApp (Contoh: 0812...)" class="w-full px-4 py-3 rounded-lg border focus:ring-2 focus:ring-blue-500 outline-none" required>
                           
                            <button type="submit" id="btnSubmit" class="w-full bg-sunset text-white font-bold py-4 rounded-lg hover:bg-orange-600 transition shadow-lg text-lg flex items-center justify-center gap-2">
                                <span id="btnText">Dapatkan Itinerary & Harga Paket</span>
                            </button>
                        </form>
                    </div>
                </div>
            </div>
        </section>
    </main>

    <!-- SECTION: DASHBOARD (ADMIN) -->
    <main id="section-dashboard" class="page-section pt-32 pb-20">
        <div class="container mx-auto px-6">
            <div class="bg-white rounded-2xl shadow-xl overflow-hidden">
                <div class="bg-ocean p-8 flex flex-col md:flex-row justify-between items-center gap-4 text-white">
                    <div>
                        <h2 class="text-3xl font-bold">Admin Dashboard</h2>
                        <p class="opacity-80">Monitoring data calon pelanggan secara real-time</p>
                    </div>
                    <button onclick="loadDashboardData()" class="bg-white text-blue-900 px-6 py-2 rounded-lg font-bold hover:bg-gray-100 flex items-center gap-2 transition">
                        <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
                            <path fill-rule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd" />
                        </svg>
                        Refresh Data
                    </button>
                </div>

                <div class="overflow-x-auto">
                    <table class="w-full text-left border-collapse">
                        <thead>
                            <tr class="bg-gray-50 border-b">
                                <th class="px-6 py-4 font-bold text-gray-700">Waktu Masuk</th>
                                <th class="px-6 py-4 font-bold text-gray-700">Nama Lengkap</th>
                                <th class="px-6 py-4 font-bold text-gray-700">Email</th>
                                <th class="px-6 py-4 font-bold text-gray-700">WhatsApp</th>
                            </tr>
                        </thead>
                        <tbody id="table-leads">
                            <tr>
                                <td colspan="4" class="px-6 py-20 text-center text-gray-400 italic">
                                    Silakan klik "Refresh Data" untuk memuat isian pelanggan.
                                </td>
                            </tr>
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </main>

    <!-- Modals -->
    <div id="modalSuccess" class="fixed inset-0 bg-black/70 z-[60] hidden items-center justify-center p-4">
        <div class="bg-white p-8 rounded-2xl max-w-sm w-full text-center">
            <div class="text-green-500 text-6xl mb-4"></div>
            <h4 class="text-2xl font-bold mb-2">Berhasil Terkirim!</h4>
            <p class="text-gray-600 mb-6">Tim kami akan segera menghubungi Anda melalui WhatsApp.</p>
            <button onclick="toggleModal('modalSuccess', false)" class="w-full bg-ocean text-white py-3 rounded-xl font-bold">Siap, Terima Kasih</button>
        </div>
    </div>

    <script>
        // SPA Navigation
        function navigateTo(pageId) {
            document.querySelectorAll('.page-section').forEach(section => {
                section.classList.remove('page-active');
            });
            document.getElementById(`section-${pageId}`).classList.add('page-active');
           
            if (pageId === 'dashboard') {
                loadDashboardData();
            }
            window.scrollTo(0,0);
        }

        // Form Handling menggunakan google.script.run
        const form = document.getElementById('travelForm');
        const btnSubmit = document.getElementById('btnSubmit');
        const btnText = document.getElementById('btnText');

        form.addEventListener('submit', e => {
            e.preventDefault();
           
            btnSubmit.disabled = true;
            btnText.innerHTML = '<div class="loader"></div> Mengirim...';

            const payload = {
                Nama: document.getElementById('inputNama').value,
                Email: document.getElementById('inputEmail').value,
                WhatsApp: document.getElementById('inputWA').value
            };

            // Memanggil fungsi saveLead di Code.gs
            google.script.run
                .withSuccessHandler(() => {
                    toggleModal('modalSuccess', true);
                    form.reset();
                    btnSubmit.disabled = false;
                    btnText.innerHTML = 'Dapatkan Itinerary & Harga Paket';
                })
                .withFailureHandler(err => {
                    alert('Gagal mengirim data: ' + err);
                    btnSubmit.disabled = false;
                    btnText.innerHTML = 'Dapatkan Itinerary & Harga Paket';
                })
                .saveLead(payload);
        });

        // Dashboard Data Loading menggunakan google.script.run
        function loadDashboardData() {
            const tbody = document.getElementById('table-leads');
            tbody.innerHTML = '<tr><td colspan="4" class="px-6 py-20 text-center text-gray-500"><div class="loader mb-2"></div><br>Mengambil data dari Google Sheets...</td></tr>';

            google.script.run
                .withSuccessHandler(data => {
                    if (!data || data.length === 0) {
                        tbody.innerHTML = '<tr><td colspan="4" class="px-6 py-20 text-center text-gray-400">Belum ada data pendaftar.</td></tr>';
                        return;
                    }

                    tbody.innerHTML = '';
                    data.reverse().forEach(lead => {
                        const dateFormatted = lead.Timestamp ? new Date(lead.Timestamp).toLocaleString('id-ID') : '-';
                        tbody.innerHTML += `
                            <tr class="border-b hover:bg-gray-50 transition">
                                <td class="px-6 py-4 text-sm text-gray-500 font-mono">${dateFormatted}</td>
                                <td class="px-6 py-4 font-bold text-gray-800">${lead.Nama || '-'}</td>
                                <td class="px-6 py-4 text-gray-600">${lead.Email || '-'}</td>
                                <td class="px-6 py-4">
                                    <a href="https://wa.me/${String(lead.WhatsApp).replace(/\D/g,'')}" target="_blank" class="text-green-600 font-semibold hover:underline">
                                        ${lead.WhatsApp || '-'}
                                    </a>
                                </td>
                            </tr>
                        `;
                    });
                })
                .withFailureHandler(err => {
                    tbody.innerHTML = '<tr><td colspan="4" class="px-6 py-20 text-center text-red-500">Gagal memuat data.</td></tr>';
                })
                .getLeads();
        }

        function toggleModal(id, show) {
            const modal = document.getElementById(id);
            modal.style.display = show ? 'flex' : 'none';
        }
    </script>
</body>
</html>

Tidak ada komentar:

Posting Komentar

Web dengan Firebase

  https://www.youtube.com/watch?v=_KgCFwWTORI&list=PLJTyZKho7eUhpYMoKmYq9aeU20dRiWVes https://www.youtube.com/watch?v=2CPE5yKzMqE