Buatkan aplikasi Studi Kasus (Final UI) Aplikasi News Portal ✔ SPA ✔ CRUD Lengkap ✔ Bootstrap 5 ✔ Hero Section ✔ Responsive
https://docs.google.com/spreadsheets/d/1f4qRC-_vELEhSLSVn3NvRrd-PsZp1vhwIWDra7nA7c8/edit?gid=0#gid=0
Code.gs
/*************************
* KONFIGURASI
*************************/
const SPREADSHEET_ID = '1f4qRC-_vELEhSLSVn3NvRrd-PsZp1vhwIWDra7nA7c8';
const SHEET_NAME = 'News';
/*************************
* ENTRY POINT
*************************/
function doGet() {
return HtmlService.createHtmlOutputFromFile('index')
.setTitle('News Portal')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
.addMetaTag('viewport', 'width=device-width, initial-scale=1');
}
/*************************
* HELPER
*************************/
function getSheet() {
const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
const sheet = ss.getSheetByName(SHEET_NAME);
if (!sheet) throw new Error('Sheet "News" tidak ditemukan');
return sheet;
}
/*************************
* READ
*************************/
function getNews() {
const sheet = getSheet();
const lastRow = sheet.getLastRow();
if (lastRow < 2) return [];
const range = sheet.getRange(2, 1, lastRow - 1, 5);
const values = range.getValues();
// 🔑 NORMALISASI DATA (PENTING)
return values.map(r => ({
id: String(r[0]), // paksa string
judul: String(r[1] || ''),
kategori: String(r[2] || ''),
konten: String(r[3] || ''),
tanggal: r[4]
? Utilities.formatDate(
new Date(r[4]),
Session.getScriptTimeZone(),
'yyyy-MM-dd'
)
: ''
}));
}
/*************************
* CREATE
*************************/
function addNews(n) {
const sheet = getSheet();
sheet.appendRow([
Date.now().toString(), // simpan ID sebagai STRING
n.judul,
n.kategori,
n.konten,
new Date()
]);
return true;
}
/*************************
* UPDATE
*************************/
function updateNews(n) {
const sheet = getSheet();
const ids = sheet.getRange(2, 1, sheet.getLastRow() - 1, 1).getValues();
for (let i = 0; i < ids.length; i++) {
if (String(ids[i][0]) === String(n.id)) {
sheet.getRange(i + 2, 2, 1, 3).setValues([[
n.judul,
n.kategori,
n.konten
]]);
return true;
}
}
return false;
}
/*************************
* DELETE
*************************/
function deleteNews(id) {
const sheet = getSheet();
const ids = sheet.getRange(2, 1, sheet.getLastRow() - 1, 1).getValues();
for (let i = 0; i < ids.length; i++) {
if (String(ids[i][0]) === String(id)) {
sheet.deleteRow(i + 2);
return true;
}
}
return false;
}
Index.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<!-- Bootstrap -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body { background: #f8f9fa; }
.hero {
background: linear-gradient(120deg, #212529, #0d6efd);
color: white;
padding: 90px 20px;
border-radius: 0 0 40px 40px;
}
.card { border-radius: 16px; }
</style>
</head>
<body>
<!-- HERO -->
<section class="hero text-center">
<div class="container">
<h1 class="display-5 fw-bold">News Portal</h1>
<p class="lead">SPA berbasis Google Apps Script</p>
</div>
</section>
<!-- CONTENT -->
<div class="container mt-5">
<div class="row">
<!-- FORM -->
<div class="col-md-4 mb-4">
<div class="card shadow-sm">
<div class="card-body">
<h5 class="card-title mb-3">Form Berita</h5>
<input type="hidden" id="id">
<input id="judul" class="form-control mb-2" placeholder="Judul">
<input id="kategori" class="form-control mb-2" placeholder="Kategori">
<textarea id="konten" class="form-control mb-3" rows="4" placeholder="Konten"></textarea>
<div class="d-grid gap-2">
<button class="btn btn-primary" onclick="simpan()">Simpan</button>
<button class="btn btn-outline-secondary" onclick="resetForm()">Batal</button>
</div>
</div>
</div>
</div>
<!-- NEWS LIST -->
<div class="col-md-8">
<div class="row" id="newsList"></div>
</div>
</div>
</div>
<script>
let news = [];
let modeEdit = false;
document.addEventListener('DOMContentLoaded', loadNews);
function loadNews() {
google.script.run
.withSuccessHandler(data => {
news = Array.isArray(data) ? data : [];
render();
})
.withFailureHandler(err => {
alert('Gagal load data: ' + err.message);
})
.getNews();
}
function render() {
const el = document.getElementById('newsList');
el.innerHTML = '';
if (news.length === 0) {
el.innerHTML = `<p class="text-muted">Belum ada berita</p>`;
return;
}
news.forEach(n => {
el.innerHTML += `
<div class="col-md-6 mb-4">
<div class="card h-100 shadow-sm">
<div class="card-body">
<span class="badge bg-primary">${n.kategori}</span>
<h5 class="mt-2">${n.judul}</h5>
<p class="text-muted small">${n.tanggal}</p>
<p>${n.konten.slice(0, 120)}...</p>
</div>
<div class="card-footer text-end bg-white">
<button class="btn btn-sm btn-warning" onclick="editNews('${n.id}')">Edit</button>
<button class="btn btn-sm btn-danger" onclick="hapusNews('${n.id}')">Hapus</button>
</div>
</div>
</div>`;
});
}
function simpan() {
const data = {
id: document.getElementById('id').value,
judul: judul.value,
kategori: kategori.value,
konten: konten.value
};
const cb = () => {
resetForm();
loadNews();
};
modeEdit
? google.script.run.withSuccessHandler(cb).updateNews(data)
: google.script.run.withSuccessHandler(cb).addNews(data);
}
function editNews(id) {
const n = news.find(x => x.id === id);
if (!n) return;
idInput.value = n.id;
judul.value = n.judul;
kategori.value = n.kategori;
konten.value = n.konten;
modeEdit = true;
}
function hapusNews(id) {
if (!confirm('Hapus berita ini?')) return;
google.script.run.withSuccessHandler(loadNews).deleteNews(id);
}
function resetForm() {
idInput.value = '';
judul.value = '';
kategori.value = '';
konten.value = '';
modeEdit = false;
}
const idInput = document.getElementById('id');
const judul = document.getElementById('judul');
const kategori = document.getElementById('kategori');
const konten = document.getElementById('konten');
</script>
</body>
</html>


Tidak ada komentar:
Posting Komentar