Rabu, 08 April 2026

Gemini - Kursus 3

 





Dokumentasi

https://drive.google.com/file/d/18p9OHHew6DX6TNAjL4QvvDsoF6K8iKW9/view?usp=drive_link

PROMPT

Dokumentasi Spesifikasi & Prompt Aplikasi DevAcademy

Dokumen ini merangkumi keseluruhan logik dan seni bina aplikasi SPA (Single Page Application) yang mengintegrasikan Landing Page dengan Google Sheets melalui Google Apps Script.

1. Konsep Utama & Seni Bina

Aplikasi ini dibina sebagai Single Page Application (SPA) yang mempunyai dua paparan utama dalam satu fail index.html:

  • Halaman Utama (Landing Page): Mengandungi maklumat kursus dan borang pendaftaran.

  • Dashboard Admin: Paparan jadual real-time untuk memantau pendaftar.

2. Tindanan Teknologi (Tech Stack)

  • Frontend: HTML5, Tailwind CSS (Styling), Lucide Icons (Ikonografi).

  • Backend: Google Apps Script (GAS) sebagai API penghubung ke Google Sheets.

  • Database: Google Sheets (Penyimpanan data pendaftar).

3. Penyelesaian Isu CORS (Mode No-Cors)

Ini adalah bahagian paling kritikal untuk memastikan aplikasi berfungsi apabila di-embed dalam Google Sites:

Logika Penghantaran Data (doPost)

  • Masalah: Google Sites menjalankan kod dalam iframe yang disekat, menyebabkan ralat CORS jika menghantar JSON.

  • Penyelesaian: 1. Menggunakan fetch dengan mode: 'no-cors'. 2. Menukar format data daripada JSON kepada URLSearchParams (x-www-form-urlencoded). 3. Kod di sisi Apps Script menggunakan e.parameter (bukannya e.postData.contents) untuk membaca data.

  • Kesan: Browser tidak dapat membaca respon sukses/gagal daripada server, jadi UI secara automatik memaparkan mesej kejayaan selepas proses fetch selesai.

Logika Pengambilan Data (doGet)

  • Digunakan oleh Dashboard Admin untuk menarik data JSON daripada Google Sheets secara langsung.

4. Struktur Data Google Sheets

Header yang diperlukan pada baris pertama Sheet: Timestamp | Nama Lengkap | Email | WhatsApp | Paket

5. Ringkasan Kod Google Apps Script (Code.gs)

function doGet(e) {
  // Logika untuk mengambil data JSON untuk dashboard
}

function doPost(e) {
  // Logika untuk menerima parameter form (e.parameter) dan append ke Sheet
}

6. Langkah Deploy

  1. Google Sheets: Sediakan header.

  2. Apps Script: Masukkan kod doGet dan doPost.

  3. Deployment: Pilih "Web App", akses set kepada "Anyone".

  4. index.html: Masukkan SCRIPT_URL hasil daripada deployment.

  5. Google Sites: Gunakan "Embed Code" untuk memasukkan keseluruhan fail index.html.

Status Aplikasi: Siap untuk pengeluaran (Production Ready) dengan sokongan penuh integrasi Google Sites.

7. Prompt untuk Pembuatan Frontend (Reusable Prompt)

Gunakan prompt berikut jika Anda ingin menghasilkan ulang kode frontend di masa mendatang:

Prompt: "Buatkan sebuah aplikasi web satu file (index.html) menggunakan arsitektur SPA dengan Tailwind CSS dan Lucide Icons. Aplikasi terdiri dari:

  1. Landing Page: Tema kursus Web Development 'DevAcademy'. Miliki Hero Section (Headline: 'Kuasai Web Development dalam 12 Minggu'), fitur kurikulum, dan formulir pendaftaran (Nama, Email, WhatsApp, Paket).

  2. Integrasi Backend: Gunakan URL variabel SCRIPT_URL. Implementasikan pengiriman form menggunakan fetch dengan mode: 'no-cors' dan format body URLSearchParams (x-www-form-urlencoded) agar kompatibel dengan Google Sites Embed. Tampilkan pesan sukses secara otomatis setelah pengiriman.

  3. Dashboard Admin: Tambahkan sistem navigasi untuk pindah ke view 'Admin'. Dashboard harus memanggil SCRIPT_URL menggunakan fetch (doGet) untuk menampilkan data pendaftar dalam tabel responsif. Sertakan fitur pencarian (search) dan tombol ekspor ke CSV.

  4. Aesthetics: Gunakan font 'Plus Jakarta Sans', skema warna biru/emerald profesional, dan efek transisi antar view yang mulus."

Status Aplikasi: Siap untuk pengeluaran (Production Ready) dengan sokongan penuh integrasi Google Sites.

8. Prompt untuk Pembuatan Backend (Reusable Prompt)

Gunakan prompt berikut untuk menghasilkan skrip Google Apps Script yang sesuai dengan kebutuhan sistem ini:

Prompt: "Buatkan skrip Google Apps Script (Code.gs) untuk backend aplikasi pendaftaran kursus. Skrip harus memiliki fungsi berikut:

  1. doPost(e): Berfungsi untuk menerima data pendaftaran. Karena frontend menggunakan mode 'no-cors', data akan dikirim dalam format URL-encoded. Ambil data (nama, email, whatsapp, paket) menggunakan e.parameter. Tambahkan data tersebut ke baris baru di Google Sheet bersama dengan Timestamp saat ini. Pastikan mengembalikan ContentService.createTextOutput("Success").

  2. doGet(e): Berfungsi untuk Dashboard Admin. Ambil seluruh data dari sheet, konversi menjadi array of objects (JSON), di mana baris pertama sheet adalah kuncinya. Format kolom 'Timestamp' menjadi string tanggal yang mudah dibaca (dd/MM/yyyy HH:mm). Kembalikan sebagai ContentService.MimeType.JSON.

  3. setupSheet(): Fungsi untuk inisialisasi awal. Buat header di baris pertama: 'Timestamp', 'Nama Lengkap', 'Email', 'WhatsApp', dan 'Paket'. Beri format teks tebal (bold) dan bekukan (freeze) baris pertama tersebut.

  4. Keamanan: Tambahkan blok try-catch sederhana pada fungsi doPost untuk menangani error saat penulisan data."

Status Aplikasi: Siap untuk pengeluaran (Production Ready) dengan sokongan penuh integrasi Google Sites.

Code.gs

/**
 * Script Backend DevAcademy
 * Menangani pendaftaran siswa (doPost) dan pengambilan data dashboard (doGet)
 */

// 1. Fungsi untuk Menampilkan Data (Dashboard Admin)
// Dipanggil saat dashboard admin melakukan fetch(SCRIPT_URL)
function doGet(e) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  const data = sheet.getDataRange().getValues();
 
  // Jika hanya ada header atau kosong
  if (data.length < 2) {
    return ContentService.createTextOutput(JSON.stringify([]))
      .setMimeType(ContentService.MimeType.JSON);
  }

  const headers = data[0]; // Baris pertama sebagai kunci JSON
  const rows = data.slice(1); // Data pendaftar

  const result = rows.map(row => {
    const obj = {};
    headers.forEach((header, i) => {
      let value = row[i];
     
      // Format khusus untuk tanggal agar mudah dibaca di tabel
      if (header === "Timestamp" && value instanceof Date) {
        value = Utilities.formatDate(value, Session.getScriptTimeZone(), "dd/MM/yyyy HH:mm");
      }
     
      obj[header] = value;
    });
    return obj;
  });

  // Mengembalikan data dalam format JSON
  return ContentService.createTextOutput(JSON.stringify(result))
    .setMimeType(ContentService.MimeType.JSON);
}

// 2. Fungsi untuk Menyimpan Data (Pendaftaran Siswa)
// Dipanggil saat formulir pendaftaran dikirimkan
function doPost(e) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
 
  // Mengambil parameter dari form (x-www-form-urlencoded)
  const p = e.parameter;
 
  try {
    // Menambahkan baris baru ke Google Sheets
    // Urutan kolom: Timestamp, Nama Lengkap, Email, WhatsApp, Paket
    sheet.appendRow([
      new Date(),
      p.nama || "Tanpa Nama",
      p.email || "Tanpa Email",
      p.whatsapp || "Tanpa WA",
      p.paket || "Tanpa Paket"
    ]);

    return ContentService.createTextOutput("Success")
      .setMimeType(ContentService.MimeType.TEXT);
     
  } catch (err) {
    return ContentService.createTextOutput("Error: " + err.toString())
      .setMimeType(ContentService.MimeType.TEXT);
  }
}

/**
 * Fungsi Pembantu: Setup Database
 * Jalankan fungsi ini sekali di editor Apps Script untuk membuat header otomatis
 */
function setupSheet() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  sheet.clear();
 
  // Menentukan header (Harus sama persis dengan kunci di dashboard admin)
  const headers = ["Timestamp", "Nama Lengkap", "Email", "WhatsApp", "Paket"];
 
  sheet.appendRow(headers);
 
  // Mempercantik header
  const range = sheet.getRange(1, 1, 1, headers.length);
  range.setFontWeight("bold");
  range.setBackground("#f3f4f6");
  range.setHorizontalAlignment("center");
 
  // Membekukan baris pertama agar tidak ikut ter-scroll
  sheet.setFrozenRows(1);
}

Index.html

<!DOCTYPE html>
<html lang="id" class="scroll-smooth">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>DevAcademy | Kursus Web Development</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <script src="https://unpkg.com/lucide@latest"></script>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
    <style>
        body { font-family: 'Plus Jakarta Sans', sans-serif; }
        .gradient-text {
            background: linear-gradient(90deg, #3b82f6, #10b981);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
        }
        .view-transition { transition: all 0.3s ease-in-out; }
        [v-cloak] { display: none; }
    </style>
</head>
<body class="bg-slate-50 text-slate-900">

    <!-- Navigation -->
    <nav class="fixed w-full z-50 bg-white/80 backdrop-blur-md border-b border-slate-200">
        <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
            <div class="flex justify-between h-16 items-center">
                <div class="flex items-center gap-2 cursor-pointer" onclick="showView('home')">
                    <div class="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center">
                        <i data-lucide="code-2" class="text-white w-5 h-5"></i>
                    </div>
                    <span class="font-bold text-xl tracking-tight">Dev<span class="text-blue-600">Academy</span></span>
                </div>
                <div id="nav-links" class="hidden md:flex space-x-8 font-medium text-slate-600">
                    <a href="#kurikulum" class="hover:text-blue-600 transition">Kurikulum</a>
                    <a href="#keunggulan" class="hover:text-blue-600 transition">Keunggulan</a>
                    <a href="#harga" class="hover:text-blue-600 transition">Harga</a>
                </div>
                <div class="flex items-center gap-4">
                    <button onclick="showView('admin')" class="text-slate-500 hover:text-blue-600 font-medium text-sm flex items-center gap-1">
                        <i data-lucide="layout-dashboard" class="w-4 h-4"></i> Dashboard Admin
                    </button>
                    <a href="#daftar" id="nav-cta" class="bg-blue-600 text-white px-5 py-2.5 rounded-full font-semibold hover:bg-blue-700 transition shadow-lg shadow-blue-200 text-sm">Daftar</a>
                </div>
            </div>
        </div>
    </nav>

    <!-- VIEWS CONTAINER -->
    <main id="app-container">
       
        <!-- HOME VIEW (Landing Page & Form) -->
        <div id="view-home" class="view-content view-transition">
            <!-- Hero Section -->
            <section class="pt-32 pb-20 overflow-hidden">
                <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
                    <div class="grid lg:grid-cols-2 gap-12 items-center">
                        <div class="space-y-8">
                            <div class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-blue-50 text-blue-700 text-sm font-semibold border border-blue-100">
                                <span class="flex h-2 w-2 rounded-full bg-blue-600 animate-pulse"></span>
                                Batch Mei: Sisa 5 Kursi Lagi!
                            </div>
                            <h1 class="text-5xl md:text-6xl font-extrabold leading-tight tracking-tight text-slate-900">
                                Kuasai Web Development dalam <span class="gradient-text">12 Minggu.</span>
                            </h1>
                            <p class="text-lg text-slate-600 leading-relaxed max-w-xl">
                                Bangun portofolio riil dari nol hingga siap kerja. Kurikulum berbasis industri dengan bimbingan mentor berpengalaman.
                            </p>
                            <div class="flex flex-col sm:flex-row gap-4 pt-4">
                                <a href="#daftar" class="bg-orange-500 text-white px-8 py-4 rounded-xl font-bold text-lg hover:bg-orange-600 transition shadow-xl shadow-orange-200 text-center">
                                    Daftar Sekarang
                                </a>
                                <a href="#kurikulum" class="bg-white text-slate-700 border-2 border-slate-200 px-8 py-4 rounded-xl font-bold text-lg hover:border-blue-600 hover:text-blue-600 transition text-center flex items-center justify-center gap-2">
                                    Lihat Silabus <i data-lucide="arrow-right" class="w-5 h-5"></i>
                                </a>
                            </div>
                        </div>
                        <div class="relative">
                            <div class="bg-white p-4 rounded-2xl shadow-2xl border border-slate-100 rotate-2">
                                <img src="https://images.unsplash.com/photo-1498050108023-c5249f4df085?auto=format&fit=crop&w=800&q=80" alt="Coding Showcase" class="rounded-xl">
                            </div>
                        </div>
                    </div>
                </div>
            </section>

            <!-- Registration Form -->
            <section id="daftar" class="py-24 bg-white border-t border-slate-100">
                <div class="max-w-xl mx-auto px-4">
                    <div class="bg-white p-8 md:p-12 rounded-3xl shadow-2xl border border-slate-100">
                        <div class="text-center mb-8">
                            <h2 class="text-3xl font-bold mb-2">Mulai Perjalananmu</h2>
                            <p class="text-slate-500 italic">"Garansi uang kembali dalam 7 hari jika materi tidak sesuai."</p>
                        </div>
                       
                        <form id="registrationForm" class="space-y-5">
                            <div>
                                <label class="block text-sm font-semibold text-slate-700 mb-2">Nama Lengkap</label>
                                <input type="text" name="nama" required class="w-full px-4 py-3 rounded-xl border border-slate-200 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition" placeholder="Contoh: Amir Santoso">
                            </div>
                            <div>
                                <label class="block text-sm font-semibold text-slate-700 mb-2">Alamat Email</label>
                                <input type="email" name="email" required class="w-full px-4 py-3 rounded-xl border border-slate-200 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition" placeholder="amir@email.com">
                            </div>
                            <div>
                                <label class="block text-sm font-semibold text-slate-700 mb-2">WhatsApp</label>
                                <input type="tel" name="whatsapp" required class="w-full px-4 py-3 rounded-xl border border-slate-200 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition" placeholder="0812xxxxxxxx">
                            </div>
                            <div>
                                <label class="block text-sm font-semibold text-slate-700 mb-2">Paket Pilihan</label>
                                <select name="paket" class="w-full px-4 py-3 rounded-xl border border-slate-200 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
                                    <option value="basic">Basic Class - Rp 999rb</option>
                                    <option value="pro" selected>Pro BootCamp - Rp 2.499rb</option>
                                </select>
                            </div>
                            <button type="submit" id="submitBtn" class="w-full py-4 bg-orange-600 text-white rounded-xl font-bold text-lg hover:bg-orange-700 transition shadow-xl shadow-orange-100 flex items-center justify-center gap-2">
                                <span>Konfirmasi Pendaftaran</span>
                                <i data-lucide="send" class="w-5 h-5"></i>
                            </button>
                        </form>
                        <div id="formFeedback" class="hidden mt-6 p-4 rounded-xl text-center font-medium"></div>
                    </div>
                </div>
            </section>
        </div>

        <!-- ADMIN VIEW (Dashboard) -->
        <div id="view-admin" class="view-content view-transition hidden pt-24 pb-20">
            <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
                <div class="flex flex-col md:flex-row justify-between items-start md:items-end mb-8 gap-4">
                    <div>
                        <h1 class="text-3xl font-bold text-slate-900 tracking-tight">Dashboard Pendaftar</h1>
                        <p class="text-slate-500">Data pendaftar yang tersimpan di Google Sheets secara real-time.</p>
                    </div>
                    <div class="flex gap-3">
                        <button onclick="fetchAdminData()" class="flex items-center gap-2 bg-white border border-slate-200 px-4 py-2 rounded-xl text-sm font-semibold hover:bg-slate-50 transition shadow-sm">
                            <i data-lucide="refresh-cw" class="w-4 h-4" id="refresh-icon"></i> Segarkan Data
                        </button>
                        <button onclick="exportToCSV()" class="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-xl text-sm font-semibold hover:bg-blue-700 transition">
                            <i data-lucide="download" class="w-4 h-4"></i> Export CSV
                        </button>
                    </div>
                </div>

                <div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
                    <div class="overflow-x-auto">
                        <table class="w-full text-left border-collapse" id="adminTable">
                            <thead>
                                <tr class="bg-slate-50 border-b border-slate-200">
                                    <th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider">Tanggal</th>
                                    <th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider">Nama</th>
                                    <th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider">Kontak</th>
                                    <th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider">Paket</th>
                                </tr>
                            </thead>
                            <tbody id="table-body" class="divide-y divide-slate-100">
                                <!-- Baris data akan diisi di sini -->
                            </tbody>
                        </table>
                        <div id="loading-state" class="py-20 text-center">
                            <div class="animate-spin w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full mx-auto mb-4"></div>
                            <p class="text-slate-500 font-medium">Memuat data...</p>
                        </div>
                        <div id="empty-state" class="hidden py-20 text-center">
                            <i data-lucide="inbox" class="w-12 h-12 mx-auto mb-4 text-slate-200"></i>
                            <p class="text-slate-500">Belum ada data pendaftar.</p>
                        </div>
                    </div>
                </div>
            </div>
        </div>

    </main>

    <footer class="py-12 bg-slate-50 border-t border-slate-200">
        <div class="max-w-7xl mx-auto px-4 text-center">
            <p class="text-slate-500 text-sm">&copy; 2024 DevAcademy. Powered by Google Apps Script & SPA Architecture.</p>
        </div>
    </footer>

    <script>
        // Inisialisasi Ikon Lucide
        lucide.createIcons();

        // GANTI URL INI DENGAN URL WEB APP ANDA
        const SCRIPT_URL = 'URL_WEB_APP_ANDA_DISINI';

        // 1. Logika Navigasi SPA
        window.showView = (viewName) => {
            document.querySelectorAll('.view-content').forEach(el => el.classList.add('hidden'));
            document.getElementById(`view-${viewName}`).classList.remove('hidden');
           
            if (viewName === 'admin') {
                document.getElementById('nav-links').classList.add('invisible');
                document.getElementById('nav-cta').classList.add('hidden');
                fetchAdminData();
            } else {
                document.getElementById('nav-links').classList.remove('invisible');
                document.getElementById('nav-cta').classList.remove('hidden');
            }
            window.scrollTo(0, 0);
        };

        // 2. Logika Formulir Pendaftaran (doPost)
        const regForm = document.getElementById('registrationForm');
        const feedback = document.getElementById('formFeedback');
        const submitBtn = document.getElementById('submitBtn');

        regForm.addEventListener('submit', async (e) => {
            e.preventDefault();

            if (SCRIPT_URL === 'URL_WEB_APP_ANDA_DISINI') {
                alert("Kesalahan: SCRIPT_URL belum dikonfigurasi.");
                return;
            }

            submitBtn.disabled = true;
            const originalText = submitBtn.innerHTML;
            submitBtn.innerHTML = 'Memproses...';

            // Menggunakan format x-www-form-urlencoded untuk mode no-cors
            const formData = new FormData(regForm);
            const params = new URLSearchParams();
            for (const [key, value] of formData.entries()) {
                params.append(key, value);
            }

            try {
                await fetch(SCRIPT_URL, {
                    method: 'POST',
                    mode: 'no-cors',
                    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                    body: params.toString()
                });

                // Tampilkan sukses (asumsi sukses pada mode no-cors jika tidak ada error jaringan)
                feedback.textContent = "Pendaftaran Berhasil! Kami akan segera menghubungi Anda.";
                feedback.className = "mt-6 p-4 rounded-xl text-center font-medium bg-emerald-100 text-emerald-700 block";
                regForm.reset();
               
            } catch (err) {
                console.error("Gagal mengirim:", err);
                feedback.textContent = "Gagal mengirim pendaftaran. Periksa koneksi Anda.";
                feedback.className = "mt-6 p-4 rounded-xl text-center font-medium bg-red-100 text-red-700 block";
            } finally {
                submitBtn.disabled = false;
                submitBtn.innerHTML = originalText;
                lucide.createIcons();
            }
        });

        // 3. Logika Dashboard Admin (doGet)
        async function fetchAdminData() {
            const body = document.getElementById('table-body');
            const loader = document.getElementById('loading-state');
            const empty = document.getElementById('empty-state');
            const refreshIcon = document.getElementById('refresh-icon');

            if(SCRIPT_URL === 'URL_WEB_APP_ANDA_DISINI') return;

            refreshIcon.classList.add('animate-spin');
            loader.classList.remove('hidden');
            body.innerHTML = '';
            empty.classList.add('hidden');

            try {
                const response = await fetch(SCRIPT_URL);
                const data = await response.json();

                if (data.length === 0) {
                    empty.classList.remove('hidden');
                } else {
                    // Render Baris Tabel
                    data.reverse().forEach(item => {
                        const tr = document.createElement('tr');
                        tr.className = "hover:bg-slate-50 transition-colors";
                        tr.innerHTML = `
                            <td class="px-6 py-4 text-xs text-slate-500 whitespace-nowrap">${item.Timestamp || '-'}</td>
                            <td class="px-6 py-4">
                                <div class="font-bold text-slate-900">${item["Nama Lengkap"] || '-'}</div>
                            </td>
                            <td class="px-6 py-4">
                                <div class="text-sm font-medium text-slate-700">${item.Email || '-'}</div>
                                <div class="text-xs text-slate-400">${item.WhatsApp || '-'}</div>
                            </td>
                            <td class="px-6 py-4">
                                <span class="px-2 py-1 rounded-md text-[10px] font-bold uppercase ${item.Paket === 'pro' ? 'bg-blue-100 text-blue-700' : 'bg-orange-100 text-orange-700'}">
                                    ${item.Paket || '-'}
                                </span>
                            </td>
                        `;
                        body.appendChild(tr);
                    });
                }
            } catch (error) {
                console.error('Fetch error:', error);
                body.innerHTML = '<tr><td colspan="4" class="p-8 text-center text-red-500">Gagal memuat data. Periksa konfigurasi Apps Script.</td></tr>';
            } finally {
                loader.classList.add('hidden');
                refreshIcon.classList.remove('animate-spin');
                lucide.createIcons();
            }
        }

        // 4. Ekspor ke CSV
        window.exportToCSV = () => {
            const rows = Array.from(document.querySelectorAll('#adminTable tr'));
            let csvContent = "data:text/csv;charset=utf-8,Tanggal,Nama,Email,WhatsApp,Paket\n";
           
            rows.slice(1).forEach(row => {
                const cols = row.querySelectorAll('td');
                const rowData = Array.from(cols).map(c => `"${c.innerText.replace(/\n/g, ' ')}"`).join(",");
                csvContent += rowData + "\n";
            });

            const encodedUri = encodeURI(csvContent);
            const link = document.createElement("a");
            link.setAttribute("href", encodedUri);
            link.setAttribute("download", "pendaftar_devacademy.csv");
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
        };
    </script>
</body>
</html>

Google Site


Web dengan Firebase

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