Senin, 06 April 2026

Web Technology

 

Front End




Back End




JavaScript - Latihan 1

 






https://www.geeksforgeeks.org/javascript/javascript-tutorial/

Prompt Buku Tamu

 







/**
 * Konfigurasi Aplikasi
 * Pastikan ID Folder dan ID Template sudah benar.
 */
const CONFIG = {
  FOLDER_ID: '1S02hUM3cdjE-9Oc8nr-8FOh8nw_bCusB',
  TEMPLATE_ID: '1JLAQNVQupESFzfc1QAG01O3-rNS0bYxO_ZVC05Yr9xE',
  SHEET_NAME: 'DataTamu'
};


PROMPT

1. Saya seorang programmer google apps script. Buatkan spesifikasi aplikasi pencatatan buku tamu. Di dalamnya ada landing page, dan juga upload foto yang diupload ke google drive. Kemudian ada dashboard dan pencetakan file pdf.

2. Buatkan prompt untuk membuat aplikasi sesuai dengan spesifikasi tersebut

Prompt: Pembuatan Aplikasi Buku Tamu Digital (GAS)

Konteks & Peran

Bertindaklah sebagai Senior Full-stack Developer spesialis Google Apps Script (GAS). Bangun aplikasi Buku Tamu Digital yang terintegrasi dengan Google Sheets, Drive, dan Docs.

Arsitektur & Fitur Utama

  1. Frontend (SPA):

    • Gunakan Bootstrap 5 untuk UI yang modern dan responsif.

    • Landing Page: Formulir input (Nama, Instansi, Tujuan, Staff) dan fitur Upload Foto.

    • Client-side Optimization: Gunakan elemen <canvas> untuk melakukan resize dan kompresi gambar (maks 800px, quality 0.7) sebelum dikirim sebagai Base64 guna menghindari limitasi payload GAS.

    • Dashboard Admin: Tabel interaktif untuk melihat riwayat tamu secara real-time dengan tombol "Cetak PDF".

  2. Backend (Code.gs):

    • Database: Simpan data teks ke Google Sheets (ID, Timestamp, Nama, Instansi, Tujuan, Staff, URL Foto).

    • Penyimpanan: Unggah foto Base64 ke folder Google Drive spesifik dan atur izin aksesnya agar dapat dilihat melalui link.

    • PDF Engine: * Gunakan Google Docs sebagai template.

      • Lakukan cloning template, ganti placeholder teks ({{nama}}, {{instansi}}, dll).

      • Penyisipan Foto Dinamis: Cari placeholder {{foto}} di dalam dokumen, ambil file dari Drive, dan masukkan sebagai Inline Image dengan ukuran proporsional (lebar 150-180px).

      • Konversi hasil akhir ke PDF, simpan di Drive, dan hapus file dokumen sementara.

  3. Standar Kode:

    • Gunakan objek CONFIG untuk ID Folder, ID Template, dan Nama Sheet di bagian atas kode.

    • Komunikasi client-server wajib menggunakan google.script.run dengan withSuccessHandler.

    • Terapkan error handling (try-catch) pada setiap fungsi krusial di server-side.

Spesifikasi Template Google Doc

Buat template dokumen dengan tabel yang berisi placeholder berikut:

  • {{nama}}, {{instansi}}, {{tujuan}}, {{staff}}, {{tanggal}} untuk data teks.

  • {{foto}} diletakkan di dalam sel tabel untuk posisi foto yang konsisten.

Code.gs

/**
 * Konfigurasi Aplikasi
 * Pastikan ID Folder dan ID Template sudah benar.
 */
const CONFIG = {
  FOLDER_ID: '1S02hUM3cdjE-9Oc8nr-8FOh8nw_bCusB',
  TEMPLATE_ID: '1JLAQNVQupESFzfc1QAG01O3-rNS0bYxO_ZVC05Yr9xE',
  SHEET_NAME: 'DataTamu'
};

function doGet(e) {
  return HtmlService.createHtmlOutputFromFile('index')
    .setTitle('Buku Tamu Digital')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
    .addMetaTag('viewport', 'width=device-width, initial-scale=1');
}

/**
 * Memproses pendaftaran tamu (Teks & Foto)
 */
function processForm(data) {
  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    let sheet = ss.getSheetByName(CONFIG.SHEET_NAME);
   
    if (!sheet) {
      sheet = ss.insertSheet(CONFIG.SHEET_NAME);
      sheet.appendRow(["ID", "Timestamp", "Nama", "Instansi", "Tujuan", "Staff", "Foto URL"]);
    }

    const id = Utilities.getUuid();
    const timestamp = new Date();
   
    // Upload Foto
    let photoUrl = "";
    if (data.photoBase64) {
      photoUrl = uploadPhoto(data.photoBase64, `Tamu_${data.nama}_${id}`);
    }

    // Simpan ke Sheets
    sheet.appendRow([id, timestamp, data.nama, data.instansi, data.tujuan, data.staff, photoUrl]);

    return { success: true, message: "Pendaftaran berhasil!" };
  } catch (e) {
    return { success: false, message: e.toString() };
  }
}

/**
 * Upload Base64 ke Drive
 */
function uploadPhoto(base64Data, fileName) {
  const folder = DriveApp.getFolderById(CONFIG.FOLDER_ID);
  const contentType = base64Data.substring(5, base64Data.indexOf(';'));
  const bytes = Utilities.base64Decode(base64Data.split(',')[1]);
  const blob = Utilities.newBlob(bytes, contentType, fileName + ".jpg");
  const file = folder.createFile(blob);
  file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
  return file.getUrl();
}

/**
 * Ambil data untuk Dashboard
 */
function getGuestData() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(CONFIG.SHEET_NAME);
  if (!sheet) return [];
  const data = sheet.getDataRange().getDisplayValues();
  const headers = data.shift();
  return data.map(row => {
    let obj = {};
    headers.forEach((h, i) => obj[h] = row[i]);
    return obj;
  }).reverse();
}

/**
 * Membuat PDF dengan Foto Dinamis
 */
function generatePDF(rowId) {
  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = ss.getSheetByName(CONFIG.SHEET_NAME);
    const data = sheet.getDataRange().getValues();
    const headers = data[0];
    const rowIndex = data.findIndex(row => row[0] === rowId);
   
    if (rowIndex === -1) throw new Error("Data tidak ditemukan");
   
    const guest = {};
    headers.forEach((h, i) => guest[h] = data[rowIndex][i]);

    // 1. Copy Template Doc
    const templateFile = DriveApp.getFileById(CONFIG.TEMPLATE_ID);
    const tempFile = templateFile.makeCopy(`Laporan_${guest.Nama}`);
    const doc = DocumentApp.openById(tempFile.getId());
    const body = doc.getBody();

    // 2. Ganti Placeholder Teks
    body.replaceText('{{nama}}', guest.Nama);
    body.replaceText('{{instansi}}', guest.Instansi);
    body.replaceText('{{tujuan}}', guest.Tujuan);
    body.replaceText('{{staff}}', guest.Staff);
    body.replaceText('{{tanggal}}', Utilities.formatDate(new Date(guest.Timestamp), "GMT+7", "dd/MM/yyyy HH:mm"));

    // 3. Sisipkan Foto
    const photoUrl = guest["Foto URL"];
    if (photoUrl) {
      const fileId = photoUrl.match(/[-\w]{25,}/);
      if (fileId) {
        try {
          const imageBlob = DriveApp.getFileById(fileId[0]).getBlob();
          const placeholder = body.findText('{{foto}}');
         
          if (placeholder) {
            const element = placeholder.getElement();
            const parent = element.getParent();
            const image = parent.asParagraph().appendInlineImage(imageBlob);
           
            // Atur ukuran foto (Lebar 150px)
            const width = 150;
            const height = (image.getHeight() / image.getWidth()) * width;
            image.setWidth(width).setHeight(height);
           
            // Hapus tag {{foto}}
            body.replaceText('{{foto}}', '');
          }
        } catch (e) {
          body.replaceText('{{foto}}', '(Gagal memuat foto)');
        }
      }
    } else {
      body.replaceText('{{foto}}', '(Tidak ada foto)');
    }

    doc.saveAndClose();

    // 4. Konversi ke PDF
    const pdfBlob = tempFile.getAs('application/pdf');
    const folder = DriveApp.getFolderById(CONFIG.FOLDER_ID);
    const pdfFile = folder.createFile(pdfBlob).setName(`Laporan_Tamu_${guest.Nama}.pdf`);
   
    // 5. Cleanup
    tempFile.setTrashed(true);
   
    return { success: true, url: pdfFile.getDownloadUrl().replace("?e=download", "") };
  } catch (e) {
    return { success: false, message: e.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>Buku Tamu Digital</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
  <style>
    :root {
      --primary-color: #1a73e8;
      --bg-color: #f8f9fa;
    }
    body { background-color: var(--bg-color); font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
    .card { border-radius: 15px; border: none; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
    .btn-primary { background-color: var(--primary-color); border: none; padding: 10px 25px; border-radius: 8px; }
    #canvas { display: none; }
    #photo-preview { width: 100%; border-radius: 12px; display: none; margin-top: 15px; border: 1px solid #ddd; }
    .nav-custom { background: white; box-shadow: 0 2px 4px rgba(0,0,0,0.05); margin-bottom: 30px; }
    .loading-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255,255,255,0.8); display: none; align-items: center; justify-content: center; z-index: 9999; }
  </style>
</head>
<body>

<div class="loading-overlay" id="loading">
  <div class="spinner-border text-primary" role="status"></div>
  <span class="ms-2">Mohon tunggu...</span>
</div>

<nav class="navbar navbar-expand-lg nav-custom">
  <div class="container">
    <a class="navbar-brand fw-bold text-primary" href="#" onclick="showView('landing')">GUESTBOOK</a>
    <button class="btn btn-outline-primary btn-sm" onclick="showView('dashboard')">Admin Dashboard</button>
  </div>
</nav>

<div class="container mb-5">
  <!-- View: Landing Page / Form -->
  <div id="view-landing">
    <div class="row justify-content-center">
      <div class="col-md-6">
        <div class="card p-4">
          <h3 class="text-center mb-4 fw-bold">Selamat Datang</h3>
          <p class="text-muted text-center mb-4">Silakan isi data kunjungan Anda</p>
         
          <form id="guestForm">
            <div class="mb-3">
              <label class="form-label">Nama Lengkap</label>
              <input type="text" class="form-control" name="nama" required placeholder="Contoh: Budi Santoso">
            </div>
            <div class="mb-3">
              <label class="form-label">Instansi / Perusahaan</label>
              <input type="text" class="form-control" name="instansi" required placeholder="Contoh: PT. Maju Jaya">
            </div>
            <div class="mb-3">
              <label class="form-label">Tujuan Kunjungan</label>
              <input type="text" class="form-control" name="tujuan" required placeholder="Contoh: Meeting Koordinasi">
            </div>
            <div class="mb-3">
              <label class="form-label">Staff yang Ditemui</label>
              <input type="text" class="form-control" name="staff" required placeholder="Contoh: Ibu Siti (HRD)">
            </div>

            <div class="mb-4">
              <label class="form-label">Upload Foto Identitas / Tamu</label>
              <input type="file" id="photo-input" accept="image/*" class="form-control" required>
              <canvas id="canvas"></canvas>
              <img id="photo-preview" src="" alt="Pratinjau Foto">
              <div id="file-help" class="form-text">Pilih file gambar (JPG/PNG). Foto akan dikompres secara otomatis.</div>
            </div>

            <button type="submit" class="btn btn-primary w-100">Kirim Data</button>
          </form>
        </div>
      </div>
    </div>
  </div>

  <!-- View: Dashboard -->
  <div id="view-dashboard" style="display:none;">
    <div class="card p-4">
      <div class="d-flex justify-content-between align-items-center mb-4">
        <h3 class="fw-bold mb-0">Riwayat Tamu</h3>
        <button class="btn btn-sm btn-outline-secondary" onclick="loadDashboardData()">Refresh Data</button>
      </div>
      <div class="table-responsive">
        <table class="table table-hover">
          <thead class="table-light">
            <tr>
              <th>Waktu</th>
              <th>Nama</th>
              <th>Instansi</th>
              <th>Tujuan</th>
              <th>Aksi</th>
            </tr>
          </thead>
          <tbody id="dashboard-body">
            <!-- Data dimuat di sini -->
          </tbody>
        </table>
      </div>
    </div>
  </div>
</div>

<script>
  let photoBase64 = "";
  const canvas = document.getElementById('canvas');
  const photoPreview = document.getElementById('photo-preview');
  const photoInput = document.getElementById('photo-input');
  const loading = document.getElementById('loading');

  // --- Navigasi View ---
  function showView(view) {
    document.getElementById('view-landing').style.display = view === 'landing' ? 'block' : 'none';
    document.getElementById('view-dashboard').style.display = view === 'dashboard' ? 'block' : 'none';
    if(view === 'dashboard') loadDashboardData();
  }

  // --- Logika Unggah File ---
  photoInput.addEventListener('change', function(e) {
    const file = e.target.files[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = function(event) {
      const img = new Image();
      img.onload = function() {
        // Logika Resize untuk menjaga ukuran file tetap aman di GAS (Max 800px)
        const maxWidth = 800;
        const maxHeight = 800;
        let width = img.width;
        let height = img.height;

        if (width > height) {
          if (width > maxWidth) {
            height *= maxWidth / width;
            width = maxWidth;
          }
        } else {
          if (height > maxHeight) {
            width *= maxHeight / height;
            height = maxHeight;
          }
        }

        canvas.width = width;
        canvas.height = height;
        const ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0, width, height);
       
        // Kompresi kualitas menjadi 0.7
        photoBase64 = canvas.toDataURL('image/jpeg', 0.7);
       
        photoPreview.src = photoBase64;
        photoPreview.style.display = "block";
      };
      img.src = event.target.result;
    };
    reader.readAsDataURL(file);
  });

  // --- Logika Formulir ---
  document.getElementById('guestForm').addEventListener('submit', function(e) {
    e.preventDefault();
    if(!photoBase64) return alert("Silakan pilih foto terlebih dahulu.");
   
    loading.style.display = "flex";
    const formData = {
      nama: this.nama.value,
      instansi: this.instansi.value,
      tujuan: this.tujuan.value,
      staff: this.staff.value,
      photoBase64: photoBase64
    };

    google.script.run
      .withSuccessHandler(res => {
        loading.style.display = "none";
        if(res.success) {
          alert("Terima kasih! Data Anda telah tersimpan.");
          this.reset();
          photoPreview.style.display = "none";
          photoBase64 = "";
        } else {
          alert("Error: " + res.message);
        }
      })
      .processForm(formData);
  });

  // --- Logika Dashboard ---
  function loadDashboardData() {
    loading.style.display = "flex";
    google.script.run
      .withSuccessHandler(data => {
        const tbody = document.getElementById('dashboard-body');
        tbody.innerHTML = "";
        data.forEach(row => {
          tbody.innerHTML += `
            <tr>
              <td><small>${row.Timestamp}</small></td>
              <td><strong>${row.Nama}</strong></td>
              <td>${row.Instansi}</td>
              <td>${row.Tujuan}</td>
              <td>
                <button class="btn btn-sm btn-primary" onclick="printPDF('${row.ID}')">Cetak PDF</button>
              </td>
            </tr>
          `;
        });
        loading.style.display = "none";
      })
      .getGuestData();
  }

  function printPDF(id) {
    loading.style.display = "flex";
    google.script.run
      .withSuccessHandler(res => {
        loading.style.display = "none";
        if(res.success) {
          window.open(res.url, '_blank');
        } else {
          alert("Gagal cetak: " + res.message);
        }
      })
      .generatePDF(id);
  }
</script>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

Doc Template

LAPORAN KUNJUNGAN TAMU

Program Digital Skills Kerjasama ITS - UNICEF

DATA PENGUNJUNG

FOTO IDENTITAS / TAMU

Nama Lengkap:

{{nama}}


Instansi / Perusahaan:

{{instansi}}


Tujuan Kunjungan:

{{tujuan}}

{{foto}}

Staff yang Ditemui:

{{staff}}


Waktu Kedatangan:

{{tanggal}}


Catatan Keamanan & Tata Tertib:

  1. Tamu wajib mengenakan kartu identitas selama berada di area kantor.

  2. Mohon kembalikan dokumen ini atau lapor ke resepsionis saat akan meninggalkan gedung.

  3. Selalu patuhi protokol keselamatan yang berlaku.

Petugas Penerima Tamu, {{tanggal}}

(_______________________)


Prompt News Portal








Prompt: News Portal System (Google Apps Script)

Peran: Senior Google Apps Script Developer & UI/UX Designer. Tujuan: Membangun aplikasi News Portal lengkap dengan Landing Page (Public) dan CMS (Admin) menggunakan Google Sheets sebagai database.

1. Arsitektur Database (Google Sheets)

Buat fungsi setupDatabase() untuk menyiapkan 3 sheet berikut:

  • Sheet 'Berita': ID, Tanggal, Judul, Konten (HTML/Text), Kategori, URL_Gambar, Status (Draft/Publish), Headline (Boolean).

  • Sheet 'Kategori': Nama_Kategori, Deskripsi.

  • Sheet 'Users': Username, Password_Hash, Role.

2. Logika Backend (Code.gs)

  • Routing: Gunakan doGet(e) dengan parameter ?p=admin untuk membuka dashboard, dan default ke index.html.

  • Data Handling:

    • getNewsData(): Mengambil data dari Sheets, konversi objek Date menjadi ISO String (untuk menghindari error serialisasi GAS), dan kembalikan dalam format JSON (Reverse order).

    • saveNews(data) & editNews(id, data): Menangani operasi CRUD termasuk status publikasi dan toggle headline.

    • deleteNews(id): Menghapus baris berdasarkan ID.

  • Utility: Fungsi getScriptUrl() untuk mendapatkan URL deployment secara dinamis.

3. Frontend Landing Page (index.html)

  • UI Framework: Bootstrap 5.3.3 & Lucide Icons.

  • Fitur Utama:

    • Hero Headline: Menampilkan 1 berita terbaru yang ditandai Headline=true.

    • News Grid: Grid responsif untuk berita reguler (hanya status 'Publish').

    • Category Filter: Navigasi kategori dinamis tanpa refresh halaman.

    • Live Search: Fitur pencarian judul/konten secara real-time.

    • Read More: Modal popup untuk membaca konten lengkap (mendukung line-break <br>).

  • Resilience:

    • Helper getVal() untuk menangani header kolom yang case-insensitive.

    • Penanganan Error/Empty State dengan ilustrasi ikon jika data gagal dimuat atau kosong.

    • Image Fallback jika URL gambar rusak.

4. Admin Dashboard (admin.html)

  • UI: Sidebar dengan judul "Kelola Artikel", Loading Overlay, dan Alert Notification.

  • CMS Fitur:

    • Tabel ringkas dengan badge status (Hijau untuk Publish, Kuning untuk Draft).

    • Modal Form tunggal untuk Tambah & Edit berita.

    • Dropdown Status Publikasi dan Switch Toggle untuk Headline.

    • Integrasi google.script.run untuk sinkronisasi data instan ke Spreadsheet.

5. Standar Teknis & Keamanan

  • Gunakan HtmlService.createTemplateFromFile untuk modularitas.

  • Pastikan semua input disanitasi (di sisi server/client).

  • Implementasikan simulasi data (mock data) jika aplikasi dijalankan di luar lingkungan GAS untuk keperluan debugging UI.

Code.gs

/**
 * News Portal Backend - Google Apps Script
 * Perbaikan: Konversi tipe data agar kompatibel dengan sisi Klien
 */

const SHEET_BERITA = 'Berita';
const SHEET_KATEGORI = 'Kategori';
const SHEET_USERS = 'Users';

function doGet(e) {
  const page = e.parameter.p || 'index';
  let template;
  try {
    template = HtmlService.createTemplateFromFile(page === 'admin' ? 'admin' : 'index');
    return template.evaluate()
      .setTitle('News Portal - Google Apps Script')
      .addMetaTag('viewport', 'width=device-width, initial-scale=1')
      .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
  } catch (err) {
    return HtmlService.createHtmlOutput("Error: Halaman '" + page + "' tidak ditemukan.");
  }
}

/**
 * Fungsi untuk mendapatkan URL Web App (digunakan di sidebar admin)
 */
function getScriptUrl() {
  return ScriptApp.getService().getUrl();
}

/**
 * Mengambil data berita dengan konversi Date ke String
 */
function getNewsData() {
  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = ss.getSheetByName(SHEET_BERITA);
    if (!sheet) return [];
   
    const data = sheet.getDataRange().getValues();
    if (data.length <= 1) return [];
   
    const headers = data.shift();
    const result = data.map(row => {
      let obj = {};
      headers.forEach((header, index) => {
        let value = row[index];
        // KRUSIAL: Konversi Date ke String agar tidak error saat dikirim ke browser
        if (value instanceof Date) {
          obj[header] = value.toISOString();
        } else {
          obj[header] = value;
        }
      });
      return obj;
    });

    return result.reverse();
  } catch (err) {
    console.error("Error getNewsData: " + err.message);
    return [];
  }
}

function setupDatabase() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const dbConfig = [
    { name: SHEET_BERITA, headers: ['ID', 'Tanggal', 'Judul', 'Konten', 'Kategori', 'URL_Gambar', 'Status', 'Headline'] },
    { name: SHEET_KATEGORI, headers: ['Nama_Kategori', 'Deskripsi'] },
    { name: SHEET_USERS, headers: ['Username', 'Password_Hash', 'Role'] }
  ];

  dbConfig.forEach(config => {
    let sheet = ss.getSheetByName(config.name);
    if (!sheet) {
      sheet = ss.insertSheet(config.name);
      sheet.getRange(1, 1, 1, config.headers.length).setValues([config.headers]).setFontWeight("bold");
      sheet.setFrozenRows(1);
    }
  });
  return "Database siap.";
}

function saveNews(newsData) {
  try {
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_BERITA);
    const newId = "NEWS-" + new Date().getTime();
    sheet.appendRow([
      newId,
      new Date(),
      newsData.judul,
      newsData.konten,
      newsData.kategori,
      newsData.imageURL,
      newsData.status || "Publish",
      newsData.isHeadline
    ]);
    return { success: true, message: "Berita berhasil disimpan!" };
  } catch (err) {
    return { success: false, message: err.message };
  }
}

function editNews(id, newsData) {
  try {
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_BERITA);
    const data = sheet.getDataRange().getValues();
    for (let i = 1; i < data.length; i++) {
      if (data[i][0].toString() === id.toString()) {
        const rowNum = i + 1;
        sheet.getRange(rowNum, 3, 1, 6).setValues([[
          newsData.judul,
          newsData.konten,
          newsData.kategori,
          newsData.imageURL,
          newsData.status,
          newsData.isHeadline
        ]]);
        return { success: true, message: "Berita diperbarui!" };
      }
    }
  } catch (err) {
    return { success: false, message: err.message };
  }
}

function deleteNews(id) {
  try {
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_BERITA);
    const data = sheet.getDataRange().getValues();
    for (let i = 1; i < data.length; i++) {
      if (data[i][0].toString() === id.toString()) {
        sheet.deleteRow(i + 1);
        return { success: true, message: "Berita dihapus." };
      }
    }
  } catch (err) {
    return { success: false, message: err.message };
  }
}

index.html

<!DOCTYPE html>
<html lang="id">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>News Portal - Informasi Terpercaya</title>
  <!-- Bootstrap 5.3.3 CSS -->
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
  <!-- Google Fonts -->
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
  <!-- Lucide Icons -->
  <script src="https://unpkg.com/lucide@latest"></script>
  <style>
    :root {
      --primary-color: #0d6efd;
      --dark-color: #1a1d20;
    }
    body {
      font-family: 'Inter', sans-serif;
      background-color: #f8f9fa;
      color: #333;
    }
    .navbar {
      box-shadow: 0 2px 10px rgba(0,0,0,0.1);
    }
    .navbar-brand {
      font-weight: 700;
      letter-spacing: -1px;
    }
    .hero-section {
      background: #fff;
      padding: 3rem 0;
      border-bottom: 1px solid #eee;
      display: none; /* Muncul jika ada headline */
    }
    .headline-card {
      position: relative;
      overflow: hidden;
      border-radius: 20px;
      height: 500px;
      cursor: pointer;
      transition: all 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
      background-color: var(--dark-color);
    }
    .headline-card:hover {
      transform: translateY(-5px);
      box-shadow: 0 20px 40px rgba(0,0,0,0.2);
    }
    .headline-img {
      width: 100%;
      height: 100%;
      object-fit: cover;
      opacity: 0.6;
      transition: transform 0.5s ease;
    }
    .headline-card:hover .headline-img {
      transform: scale(1.05);
    }
    .headline-overlay {
      position: absolute;
      bottom: 0;
      left: 0;
      right: 0;
      background: linear-gradient(transparent, rgba(0,0,0,0.9));
      color: white;
      padding: 3rem;
    }
    .news-card {
      border: none;
      border-radius: 16px;
      transition: all 0.3s ease;
      height: 100%;
      background: #fff;
    }
    .news-card:hover {
      transform: translateY(-8px);
      box-shadow: 0 15px 30px rgba(0,0,0,0.08);
    }
    .news-img {
      height: 220px;
      object-fit: cover;
      border-top-left-radius: 16px;
      border-top-right-radius: 16px;
      background-color: #eee;
    }
    .badge-category {
      font-size: 0.7rem;
      text-transform: uppercase;
      font-weight: 700;
      padding: 0.4em 0.8em;
      border-radius: 50px;
    }
    .btn-read {
      border-radius: 50px;
      font-weight: 600;
      padding: 0.6rem 1.2rem;
    }
    #loading-state, #error-state, #empty-state {
      display: flex;
      flex-direction: column;
      align-items: center;
      text-align: center;
      padding: 5rem 0;
    }
    .news-full-content {
      line-height: 1.8;
      font-size: 1.15rem;
      color: #2c3e50;
    }
    .search-box {
      max-width: 400px;
      margin: 0 auto 2rem;
    }
    .error-icon { color: #dc3545; }
    .empty-icon { color: #6c757d; }
  </style>
</head>
<body>

  <!-- Navbar -->
  <nav class="navbar navbar-expand-lg navbar-dark bg-dark sticky-top">
    <div class="container">
      <a class="navbar-brand d-flex align-items-center" href="#" onclick="window.location.reload()">
        <i data-lucide="newspaper" class="me-2"></i> PORTAL BERITA
      </a>
      <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
        <span class="navbar-toggler-icon"></span>
      </button>
      <div class="collapse navbar-collapse" id="navbarNav">
        <ul class="navbar-nav ms-auto" id="nav-categories">
          <li class="nav-item"><a class="nav-link active" href="#" onclick="filterNews('Semua', event)">Semua</a></li>
          <li class="nav-item"><a class="nav-link" href="#" onclick="filterNews('Teknologi', event)">Teknologi</a></li>
          <li class="nav-item"><a class="nav-link" href="#" onclick="filterNews('Politik', event)">Politik</a></li>
          <li class="nav-item"><a class="nav-link" href="#" onclick="filterNews('Hiburan', event)">Hiburan</a></li>
          <li class="nav-item"><a class="nav-link" href="#" onclick="filterNews('Olahraga', event)">Olahraga</a></li>
        </ul>
      </div>
    </div>
  </nav>

  <!-- Hero Section (Headline) -->
  <section class="hero-section" id="headline-section">
    <div class="container">
      <div id="headline-container"></div>
    </div>
  </section>

  <!-- Main Feed -->
  <main class="container my-5">
    <div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-3">
      <h3 class="fw-bold m-0" id="section-title">Berita Terbaru</h3>
      <div class="search-box mb-0 w-100 w-md-auto">
        <div class="input-group shadow-sm rounded-pill overflow-hidden">
          <span class="input-group-text bg-white border-end-0 ps-3"><i data-lucide="search" size="18"></i></span>
          <input type="text" class="form-control border-start-0 py-2" id="search-input" placeholder="Cari berita..." onkeyup="searchNews()">
        </div>
      </div>
    </div>

    <!-- Loading State -->
    <div id="loading-state">
      <div class="spinner-border text-primary mb-3" role="status" style="width: 3rem; height: 3rem;"></div>
      <p class="text-muted fw-medium">Menyinkronkan data dengan database...</p>
    </div>

    <!-- Error State (Hidden by default) -->
    <div id="error-state" style="display: none;">
      <i data-lucide="alert-circle" class="error-icon mb-3" size="64"></i>
      <h4 class="fw-bold">Gagal Memuat Berita</h4>
      <p class="text-muted mb-4" id="error-message">Terjadi kesalahan saat mencoba mengambil data dari Google Spreadsheet.</p>
      <button class="btn btn-primary rounded-pill px-4" onclick="fetchNews()">
        <i data-lucide="refresh-cw" size="18" class="me-2"></i> Coba Lagi
      </button>
    </div>

    <!-- Empty State (Hidden by default) -->
    <div id="empty-state" style="display: none;">
      <i data-lucide="database-zap" class="empty-icon mb-3" size="64"></i>
      <h4 class="fw-bold">Belum Ada Berita</h4>
      <p class="text-muted mb-0">Database kosong atau semua berita masih berstatus Draft.</p>
    </div>

    <!-- News Grid -->
    <div class="row g-4" id="news-grid"></div>
  </main>

  <!-- Footer -->
  <footer class="bg-white py-5 border-top mt-5">
    <div class="container text-center">
      <p class="text-muted small mb-0">&copy; 2024 Portal Berita GAS. Sistem Manajemen Konten Terintegrasi.</p>
    </div>
  </footer>

  <!-- Modal Detail Berita -->
  <div class="modal fade" id="newsModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
      <div class="modal-content border-0 shadow-lg">
        <div class="modal-header border-0 pb-0">
          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
        </div>
        <div class="modal-body p-4 pt-0" id="modal-content-body">
          <!-- Konten Dinamis -->
        </div>
      </div>
    </div>
  </div>

  <!-- Scripts -->
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
  <script>
    let allNews = [];
    const IMG_PLACEHOLDER = 'https://images.unsplash.com/photo-1504711434969-e33886168f5c?q=80&w=1000&auto=format&fit=crop';

    window.onload = function() {
      lucide.createIcons();
      fetchNews();
    };

    // Helper: Mengambil nilai objek tanpa memedulikan case sensitive key
    function getVal(obj, key) {
      if (!obj) return "";
      const lowerKey = key.toLowerCase();
      const actualKey = Object.keys(obj).find(k => k.toLowerCase() === lowerKey);
      return actualKey ? obj[actualKey] : "";
    }

    function fetchNews() {
      // Reset UI States
      document.getElementById('loading-state').style.display = 'flex';
      document.getElementById('error-state').style.display = 'none';
      document.getElementById('empty-state').style.display = 'none';
      document.getElementById('news-grid').innerHTML = '';
      document.getElementById('headline-section').style.display = 'none';

      if (typeof google !== 'undefined' && google.script && google.script.run) {
        google.script.run
          .withSuccessHandler(data => {
            allNews = data;
            renderAll();
          })
          .withFailureHandler(err => {
            showError("Server Error: " + err);
          })
          .getNewsData();
      } else {
        // Mode Simulasi (Browser Biasa)
        setTimeout(() => {
          // Simulasi jika data kosong: renderAll([])
          // Simulasi jika data ada: renderAll([...])
          allNews = [
            {ID: '1', Judul: 'Eksplorasi Google Apps Script', Konten: 'Belajar cara membuat web app profesional dengan backend Google Sheets.', Kategori: 'Teknologi', IsHeadline: true, Tanggal: new Date(), ImageURL: '', Status: 'Publish'},
            {ID: '2', Judul: 'Pentingnya UI/UX di Portal Berita', Konten: 'Desain yang bersih membantu pembaca fokus pada konten berita.', Kategori: 'Hiburan', IsHeadline: false, Tanggal: new Date(), ImageURL: '', Status: 'Publish'}
          ];
          renderAll();
        }, 1200);
      }
    }

    function showError(msg) {
      document.getElementById('loading-state').style.display = 'none';
      document.getElementById('error-state').style.display = 'flex';
      document.getElementById('error-message').innerText = msg;
      lucide.createIcons();
    }

    function renderAll() {
      document.getElementById('loading-state').style.display = 'none';
     
      // Filter hanya berita dengan status 'Publish' (case insensitive)
      const publishedNews = allNews.filter(n => {
        const status = getVal(n, 'Status').toLowerCase();
        return status === 'publish' || status === ''; // Default publish jika kosong
      });

      if (publishedNews.length === 0) {
        document.getElementById('empty-state').style.display = 'flex';
        lucide.createIcons();
        return;
      }

      renderHeadline(publishedNews);
      displayNewsGrid(publishedNews.filter(n => !isHeadline(n)));
    }

    function isHeadline(item) {
      const val = getVal(item, 'IsHeadline');
      return val === true || val === "true" || val === "TRUE" || val === 1;
    }

    function renderHeadline(newsList) {
      const hContainer = document.getElementById('headline-container');
      const hSection = document.getElementById('headline-section');
      const headlines = newsList.filter(n => isHeadline(n));

      if (headlines.length > 0) {
        const top = headlines[0];
        const img = getVal(top, 'ImageURL') || getVal(top, 'URL_Gambar') || IMG_PLACEHOLDER;
        hSection.style.display = 'block';
        hContainer.innerHTML = `
          <div class="headline-card shadow" onclick="showFullNews('${getVal(top, 'ID')}')">
            <img src="${img}" class="headline-img" onerror="this.src='${IMG_PLACEHOLDER}'">
            <div class="headline-overlay">
              <span class="badge bg-danger badge-category mb-3">Breaking News</span>
              <span class="badge bg-primary badge-category mb-3 ms-1">${getVal(top, 'Kategori')}</span>
              <h1 class="display-4 fw-bold mb-2">${getVal(top, 'Judul')}</h1>
              <p class="mb-0 text-white-50"><i data-lucide="calendar" size="14" class="me-1"></i> ${formatDate(getVal(top, 'Tanggal'))}</p>
            </div>
          </div>
        `;
        lucide.createIcons();
      } else {
        hSection.style.display = 'none';
      }
    }

    function displayNewsGrid(list) {
      const grid = document.getElementById('news-grid');
      grid.innerHTML = '';

      if (list.length === 0 && document.getElementById('headline-section').style.display === 'none') {
        document.getElementById('empty-state').style.display = 'flex';
        lucide.createIcons();
        return;
      }

      list.forEach(news => {
        const img = getVal(news, 'ImageURL') || getVal(news, 'URL_Gambar') || IMG_PLACEHOLDER;
        const card = `
          <div class="col-md-6 col-lg-4">
            <div class="card news-card shadow-sm border-0">
              <img src="${img}" class="card-img-top news-img" onerror="this.src='${IMG_PLACEHOLDER}'">
              <div class="card-body d-flex flex-column p-4">
                <div class="mb-3">
                  <span class="badge bg-secondary badge-category">${getVal(news, 'Kategori')}</span>
                </div>
                <h5 class="card-title fw-bold mb-3">${getVal(news, 'Judul')}</h5>
                <p class="card-text text-muted small mb-4">
                  ${getVal(news, 'Konten').substring(0, 110)}...
                </p>
                <div class="mt-auto">
                  <button class="btn btn-outline-primary btn-read w-100 rounded-pill" onclick="showFullNews('${getVal(news, 'ID')}')">Baca Artikel</button>
                </div>
              </div>
              <div class="card-footer bg-transparent border-0 px-4 pb-4 pt-0">
                <hr class="mt-0 mb-3 opacity-10">
                <div class="d-flex justify-content-between align-items-center text-muted small">
                  <span><i data-lucide="clock" size="12" class="me-1"></i> ${formatDate(getVal(news, 'Tanggal'))}</span>
                </div>
              </div>
            </div>
          </div>
        `;
        grid.insertAdjacentHTML('beforeend', card);
      });
      lucide.createIcons();
    }

    function filterNews(kat, e) {
      if(e) e.preventDefault();
     
      const links = document.querySelectorAll('.nav-link');
      links.forEach(l => l.classList.remove('active'));
      if(e) e.target.classList.add('active');

      document.getElementById('section-title').innerText = kat === 'Semua' ? 'Berita Terbaru' : 'Kategori: ' + kat;
      document.getElementById('empty-state').style.display = 'none';

      if (kat === 'Semua') {
        renderAll();
      } else {
        document.getElementById('headline-section').style.display = 'none';
        const filtered = allNews.filter(n => getVal(n, 'Kategori') === kat && getVal(n, 'Status').toLowerCase() !== 'draft');
        displayNewsGrid(filtered);
      }
    }

    function searchNews() {
      const query = document.getElementById('search-input').value.toLowerCase();
      document.getElementById('empty-state').style.display = 'none';
     
      if (!query) {
        renderAll();
        return;
      }

      const filtered = allNews.filter(n =>
        (getVal(n, 'Judul').toLowerCase().includes(query) ||
        getVal(n, 'Konten').toLowerCase().includes(query)) &&
        getVal(n, 'Status').toLowerCase() !== 'draft'
      );
     
      document.getElementById('headline-section').style.display = 'none';
      displayNewsGrid(filtered);
    }

    function showFullNews(id) {
      const news = allNews.find(n => getVal(n, 'ID').toString() === id.toString());
      if (!news) return;

      const modalBody = document.getElementById('modal-content-body');
      const img = getVal(news, 'ImageURL') || getVal(news, 'URL_Gambar') || IMG_PLACEHOLDER;
     
      modalBody.innerHTML = `
        <div class="position-relative mb-4">
          <img src="${img}" class="img-fluid rounded-4 shadow-sm w-100" style="max-height: 450px; object-fit: cover;" onerror="this.src='${IMG_PLACEHOLDER}'">
        </div>
        <div class="px-md-2">
          <div class="mb-3 d-flex gap-2">
            <span class="badge bg-primary badge-category">${getVal(news, 'Kategori')}</span>
          </div>
          <h1 class="fw-bold mb-3 h2">${getVal(news, 'Judul')}</h1>
          <div class="d-flex align-items-center text-muted small mb-4">
            <i data-lucide="calendar" size="14" class="me-1"></i> ${formatDate(getVal(news, 'Tanggal'), true)}
          </div>
          <hr class="mb-4">
          <div class="news-full-content">
            ${getVal(news, 'Konten').replace(/\n/g, '<br><br>')}
          </div>
        </div>
      `;

      lucide.createIcons();
      const myModal = new bootstrap.Modal(document.getElementById('newsModal'));
      myModal.show();
    }

    function formatDate(dateStr, full = false) {
      if(!dateStr) return "-";
      try {
        const d = new Date(dateStr);
        if (isNaN(d.getTime())) return "-";
        return d.toLocaleDateString('id-ID', full ? { dateStyle: 'full' } : { day: 'numeric', month: 'short', year: 'numeric' });
      } catch (e) {
        return "-";
      }
    }
  </script>
</body>
</html>

admin.html

<!DOCTYPE html>
<html lang="id">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Admin Dashboard - News Portal</title>
  <!-- Bootstrap 5.3.3 CSS -->
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
  <!-- Google Fonts -->
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
  <style>
    body {
      font-family: 'Inter', sans-serif;
      background-color: #f4f7f6;
    }
    .sidebar {
      min-height: 100vh;
      background: #212529;
      color: white;
    }
    .main-content {
      padding: 2rem;
    }
    .table-container {
      background: white;
      border-radius: 10px;
      padding: 1.5rem;
      box-shadow: 0 4px 6px rgba(0,0,0,0.05);
    }
    .btn-add {
      border-radius: 50px;
      padding: 0.5rem 1.5rem;
      font-weight: 600;
    }
    #loading-overlay {
      position: fixed;
      top: 0; left: 0; width: 100%; height: 100%;
      background: rgba(255,255,255,0.8);
      display: none;
      justify-content: center;
      align-items: center;
      z-index: 9999;
    }
    .status-badge {
      font-size: 0.75rem;
      padding: 0.3rem 0.6rem;
      font-weight: 600;
    }
  </style>
</head>
<body>

  <!-- Overlay Loading -->
  <div id="loading-overlay">
    <div class="spinner-border text-primary" role="status"></div>
  </div>

  <div class="container-fluid">
    <div class="row">
      <!-- Sidebar -->
      <nav class="col-md-2 d-none d-md-block sidebar py-4">
        <div class="position-sticky">
          <h4 class="text-center mb-4 fw-bold text-primary">Kelola Artikel</h4>
          <ul class="nav flex-column">
            <li class="nav-item">
              <a class="nav-link text-white active" href="#">Dashboard</a>
            </li>
            <li class="nav-item">
              <a class="nav-link text-white-50" href="#" id="link-website" style="cursor: pointer;">Lihat Website</a>
            </li>
          </ul>
        </div>
      </nav>

      <!-- Konten Utama -->
      <main class="col-md-10 ms-sm-auto main-content">
        <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-4 border-bottom">
          <h1 class="h2 fw-bold">Manajemen Berita</h1>
          <button class="btn btn-primary btn-add" onclick="openAddModal()">+ Tambah Berita Baru</button>
        </div>

        <!-- Wadah Notifikasi -->
        <div id="alert-container"></div>

        <!-- Tabel Konten -->
        <div class="table-container">
          <div class="table-responsive">
            <table class="table table-hover align-middle">
              <thead class="table-light">
                <tr>
                  <th>Judul Berita</th>
                  <th>Kategori</th>
                  <th>Tanggal</th>
                  <th>Status</th>
                  <th>Headline</th>
                  <th>Aksi</th>
                </tr>
              </thead>
              <tbody id="news-table-body">
                <!-- Data berita dimuat di sini -->
              </tbody>
            </table>
          </div>
        </div>
      </main>
    </div>
  </div>

  <!-- Modal Tambah/Edit Berita -->
  <div class="modal fade" id="newsModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-lg">
      <div class="modal-content border-0 shadow-lg">
        <form id="newsForm">
          <div class="modal-header bg-dark text-white">
            <h5 class="modal-title" id="modalTitle">Tambah Berita</h5>
            <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
          </div>
          <div class="modal-body p-4">
            <input type="hidden" id="newsId">
           
            <div class="mb-3">
              <label class="form-label fw-semibold">Judul Berita</label>
              <input type="text" class="form-control" id="judul" required placeholder="Masukkan judul menarik...">
            </div>

            <div class="row mb-3">
              <div class="col-md-6">
                <label class="form-label fw-semibold">Kategori</label>
                <select class="form-select" id="kategori" required>
                  <option value="" selected disabled>Pilih Kategori</option>
                  <option value="Teknologi">Teknologi</option>
                  <option value="Politik">Politik</option>
                  <option value="Hiburan">Hiburan</option>
                  <option value="Olahraga">Olahraga</option>
                </select>
              </div>
              <div class="col-md-6">
                <label class="form-label fw-semibold">URL Gambar</label>
                <input type="url" class="form-control" id="imageURL" placeholder="https://image-link.com/img.jpg">
              </div>
            </div>

            <div class="mb-3">
              <label class="form-label fw-semibold">Konten Berita</label>
              <textarea class="form-control" id="konten" rows="8" required placeholder="Tulis isi berita di sini..."></textarea>
            </div>

            <div class="row mb-3">
              <div class="col-md-6">
                <label class="form-label fw-semibold">Status Publikasi</label>
                <select class="form-select" id="status" required>
                  <option value="Publish">Publish</option>
                  <option value="Draft">Draft</option>
                </select>
              </div>
              <div class="col-md-6 d-flex align-items-end">
                <div class="form-check form-switch mb-2">
                  <input class="form-check-input" type="checkbox" id="isHeadline">
                  <label class="form-check-label fw-semibold" for="isHeadline">Jadikan Headline Utama</label>
                </div>
              </div>
            </div>
          </div>
          <div class="modal-footer bg-light">
            <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Batal</button>
            <button type="submit" class="btn btn-primary px-4">Simpan Berita</button>
          </div>
        </form>
      </div>
    </div>
  </div>

  <!-- Bootstrap 5.3.3 JS -->
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

  <script>
    let allNewsData = [];
    const newsModal = new bootstrap.Modal(document.getElementById('newsModal'));
    const newsForm = document.getElementById('newsForm');
    const loadingOverlay = document.getElementById('loading-overlay');

    window.onload = function() {
      fetchData();
     
      // Update link website jika berjalan di lingkungan GAS
      if (typeof google !== 'undefined' && google.script) {
        google.script.run.withSuccessHandler(url => {
          document.getElementById('link-website').onclick = () => window.open(url, '_blank');
        }).getScriptUrl();
      }
    };

    /**
     * Helper: Mencari nilai kolom tanpa memedulikan Case Sensitive (Judul vs judul)
     */
    function getVal(obj, key) {
      if (!obj) return "";
      const lowerKey = key.toLowerCase();
      const actualKey = Object.keys(obj).find(k => k.toLowerCase() === lowerKey);
      return actualKey ? obj[actualKey] : "";
    }

    // --- PENGELOLAAN DATA ---

    function fetchData() {
      showLoading(true);
      if (typeof google !== 'undefined' && google.script && google.script.run) {
        google.script.run
          .withSuccessHandler(data => {
            console.log("Data diterima:", data);
            renderTable(data);
          })
          .withFailureHandler(handleError)
          .getNewsData();
      } else {
        // Mode Simulasi untuk Preview di Canvas
        setTimeout(() => {
          allNewsData = [
            {ID: 'NEWS-1', Judul: 'Berita Percobaan', Kategori: 'Teknologi', Tanggal: new Date().toISOString(), Status: 'Publish', IsHeadline: true},
            {ID: 'NEWS-2', Judul: 'Draf Artikel Baru', Kategori: 'Politik', Tanggal: new Date().toISOString(), Status: 'Draft', IsHeadline: false}
          ];
          renderTable(allNewsData);
        }, 1000);
      }
    }

    function renderTable(data) {
      allNewsData = data || [];
      showLoading(false);
      const tableBody = document.getElementById('news-table-body');
      tableBody.innerHTML = '';

      if (allNewsData.length === 0) {
        tableBody.innerHTML = '<tr><td colspan="6" class="text-center py-4">Belum ada berita.</td></tr>';
        return;
      }

      allNewsData.forEach(news => {
        const id = getVal(news, 'ID');
        const judul = getVal(news, 'Judul');
        const kategori = getVal(news, 'Kategori');
        const tanggalRaw = getVal(news, 'Tanggal');
        const status = getVal(news, 'Status') || 'Publish';
        const isHeadlineVal = getVal(news, 'IsHeadline');
        const headlineChecked = isHeadlineVal === true || isHeadlineVal === "TRUE" || isHeadlineVal === "true" || isHeadlineVal === 1;

        const statusBadgeClass = status.toLowerCase() === 'publish' ? 'bg-success' : 'bg-warning text-dark';
       
        // Format Tanggal yang Aman
        let tglDisplay = "-";
        if (tanggalRaw) {
          const d = new Date(tanggalRaw);
          tglDisplay = !isNaN(d.getTime()) ? d.toLocaleDateString('id-ID') : tanggalRaw;
        }

        const row = `
          <tr>
            <td class="fw-semibold text-truncate" style="max-width: 250px;">${judul || '(Tanpa Judul)'}</td>
            <td><span class="badge bg-light text-dark border">${kategori}</span></td>
            <td><small>${tglDisplay}</small></td>
            <td><span class="badge ${statusBadgeClass} status-badge">${status.toUpperCase()}</span></td>
            <td>
              ${headlineChecked ? '<span class="badge bg-danger status-badge">HEADLINE</span>' : '<span class="text-muted small">-</span>'}
            </td>
            <td>
              <button class="btn btn-sm btn-outline-primary me-1" onclick="openEditModal('${id}')">Edit</button>
              <button class="btn btn-sm btn-outline-danger" onclick="confirmDelete('${id}')">Hapus</button>
            </td>
          </tr>
        `;
        tableBody.insertAdjacentHTML('beforeend', row);
      });
    }

    // --- AKSI FORM ---

    function openAddModal() {
      document.getElementById('modalTitle').innerText = 'Tambah Berita Baru';
      document.getElementById('newsId').value = '';
      newsForm.reset();
      document.getElementById('status').value = 'Publish';
      newsModal.show();
    }

    function openEditModal(id) {
      const news = allNewsData.find(n => getVal(n, 'ID').toString() === id.toString());
      if (!news) return;

      const isHeadlineVal = getVal(news, 'IsHeadline');
      const headlineChecked = isHeadlineVal === true || isHeadlineVal === "TRUE" || isHeadlineVal === "true" || isHeadlineVal === 1;

      document.getElementById('modalTitle').innerText = 'Edit Berita';
      document.getElementById('newsId').value = getVal(news, 'ID');
      document.getElementById('judul').value = getVal(news, 'Judul');
      document.getElementById('kategori').value = getVal(news, 'Kategori');
      document.getElementById('imageURL').value = getVal(news, 'URL_Gambar') || getVal(news, 'ImageURL');
      document.getElementById('konten').value = getVal(news, 'Konten');
      document.getElementById('status').value = getVal(news, 'Status') || 'Publish';
      document.getElementById('isHeadline').checked = headlineChecked;
     
      newsModal.show();
    }

    newsForm.onsubmit = function(e) {
      e.preventDefault();
      showLoading(true);

      const id = document.getElementById('newsId').value;
      const data = {
        judul: document.getElementById('judul').value,
        kategori: document.getElementById('kategori').value,
        imageURL: document.getElementById('imageURL').value,
        konten: document.getElementById('konten').value,
        status: document.getElementById('status').value,
        isHeadline: document.getElementById('isHeadline').checked
      };

      if (typeof google !== 'undefined' && google.script && google.script.run) {
        if (id) {
          google.script.run.withSuccessHandler(onSuccess).withFailureHandler(handleError).editNews(id, data);
        } else {
          google.script.run.withSuccessHandler(onSuccess).withFailureHandler(handleError).saveNews(data);
        }
      } else {
        setTimeout(() => onSuccess({success: true, message: 'Berhasil disimpan (Mode Simulasi)'}), 1000);
      }
    };

    function confirmDelete(id) {
      const news = allNewsData.find(n => getVal(n, 'ID').toString() === id.toString());
      const judul = getVal(news, 'Judul');
     
      if (confirm(`Apakah Anda yakin ingin menghapus berita "${judul}"?`)) {
        showLoading(true);
        if (typeof google !== 'undefined' && google.script && google.script.run) {
          google.script.run.withSuccessHandler(onSuccess).withFailureHandler(handleError).deleteNews(id);
        } else {
          onSuccess({success: true, message: 'Berhasil dihapus (Mode Simulasi)'});
        }
      }
    }

    // --- UTILITAS UI ---

    function onSuccess(response) {
      showLoading(false);
      newsModal.hide();
      if (response.success) {
        showAlert(response.message, 'success');
        fetchData();
      } else {
        showAlert(response.message, 'danger');
      }
    }

    function handleError(err) {
      showLoading(false);
      showAlert('Gagal menghubungi server: ' + err, 'danger');
    }

    function showLoading(state) {
      loadingOverlay.style.display = state ? 'flex' : 'none';
    }

    function showAlert(msg, type) {
      const container = document.getElementById('alert-container');
      container.innerHTML = `
        <div class="alert alert-${type} alert-dismissible fade show shadow-sm" role="alert">
          <strong>${type === 'success' ? 'Berhasil!' : 'Gagal!'}</strong> ${msg}
          <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
        </div>
      `;
      setTimeout(() => {
        const alert = document.querySelector('.alert');
        if (alert) {
          const bsAlert = new bootstrap.Alert(alert);
          bsAlert.close();
        }
      }, 4000);
    }
  </script>
</body>
</html>

Web Technology

  Front End https://www.geeksforgeeks.org/web-tech/web-technology/ Back End https://www.geeksforgeeks.org/web-tech/web-technology/