<!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">© 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>