Senin, 06 April 2026

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>

Tidak ada komentar:

Posting Komentar

Web Technology

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