Cara Membuat Sistem Login Member Area Terintegrasi Google Spreadsheet (Tanpa Database Server)
Memiliki halaman khusus member (member area) di blog atau website adalah salah satu cara terbaik untuk mengelola konten eksklusif, arsip file, atau komunitas pembaca. Kabar baiknya, Anda bisa membangun sistem login lengkap (Register, Login, Update Profil, dan Proteksi Halaman) hanya menggunakan Google Spreadsheet sebagai database dan Google Apps Script sebagai backend-nya.
Berikut adalah panduan langkah demi langkah cara memasang dan mengaturnya di blog Anda.
Langkah 1: Menyiapkan Google Spreadsheet & Backend (Google Apps Script)
Langkah pertama adalah membuat "database" penampung data akun member menggunakan Google Spreadsheet.
- Buat Google Spreadsheet baru di Google Drive Anda.
- Buat sebuah sheet (lembar kerja) di dalamnya dan beri nama tepat Member.
- Pada baris pertama (header tabel), buat kolom dengan urutan sebagai berikut:
- Kolom A: Timestamp
- Kolom B: Nama
- Kolom C: Email
- Kolom D: Kota
- Kolom E: Password
- Buka menu Extensions (Ekstensi) > Apps Script di bagian atas Spreadsheet.
- Hapus semua kode bawaan di dalam editor, lalu salin dan tempel kode dari File 1 di bawah ini:
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Member");
function doPost(e) {
return handleRequest(e.parameter);
}
function doGet(e) {
return handleRequest(e.parameter);
}
function handleRequest(params) {
var action = params.action;
if (action === "register") {
return registerMember(params);
} else if (action === "login") {
return loginMember(params);
} else if (action === "update") {
return updateMember(params);
}
return ContentService.createTextOutput(JSON.stringify({status: "error", message: "Aksi tidak dikenal"}))
.setMimeType(ContentService.MimeType.JSON);
}
// Fungsi bantu Hash SHA-256
function hashPassword(password) {
var rawHash = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, password, Utilities.Charset.UTF_8);
var hash = "";
var len = rawHash.length;
for (var i = 0; i !== len; i++) {
if (i >= len) break;
var byteValue = rawHash[i];
if (byteValue < 0) byteValue += 256;
var byteString = byteValue.toString(16);
if (byteString.length == 1) byteString = "0" + byteString;
hash += byteString;
}
return hash;
}
function registerMember(data) {
var nama = data.nama;
var email = data.email.toLowerCase().trim();
var kota = data.kota;
var passwordPlain = data.password;
var rows = sheet.getDataRange().getValues();
var totalRows = rows.length;
for (var i = 1; i !== totalRows; i++) {
if (i >= totalRows) break;
if (rows[i][2] === email) {
return ContentService.createTextOutput(JSON.stringify({status: "failed", message: "Email sudah terdaftar!"}))
.setMimeType(ContentService.MimeType.JSON);
}
}
var passwordHashed = hashPassword(passwordPlain);
sheet.appendRow([new Date(), nama, email, kota, passwordHashed]);
return ContentService.createTextOutput(JSON.stringify({status: "success", message: "Pendaftaran berhasil! Silakan login."}))
.setMimeType(ContentService.MimeType.JSON);
}
function loginMember(data) {
var email = data.email.toLowerCase().trim();
var passwordPlain = data.password;
var passwordHashed = hashPassword(passwordPlain);
var rows = sheet.getDataRange().getValues();
var totalRows = rows.length;
for (var i = 1; i !== totalRows; i++) {
if (i >= totalRows) break;
if (rows[i][2] === email && rows[i][4] === passwordHashed) {
return ContentService.createTextOutput(JSON.stringify({
status: "success",
message: "Login berhasil!",
nama: rows[i][1],
email: rows[i][2],
kota: rows[i][3]
})).setMimeType(ContentService.MimeType.JSON);
}
}
return ContentService.createTextOutput(JSON.stringify({status: "failed", message: "Email atau Password salah!"}))
.setMimeType(ContentService.MimeType.JSON);
}
function updateMember(data) {
var oldEmail = data.oldEmail.toLowerCase().trim();
var newNama = data.nama;
var newEmail = data.email.toLowerCase().trim();
var newKota = data.kota;
var newPasswordPlain = data.password;
var rows = sheet.getDataRange().getValues();
var totalRows = rows.length;
for (var i = 1; i !== totalRows; i++) {
if (i >= totalRows) break;
// Cari baris berdasarkan email lama
if (rows[i][2] === oldEmail) {
// Jika email diubah, cek apakah email baru sudah dipakai orang lain
if (newEmail !== oldEmail) {
for (var j = 1; j !== totalRows; j++) {
if (j >= totalRows) break;
if (rows[j][2] === newEmail) {
return ContentService.createTextOutput(JSON.stringify({status: "failed", message: "Email baru sudah digunakan akun lain!"}))
.setMimeType(ContentService.MimeType.JSON);
}
}
}
// Update data di baris tersebut (Kolom: Nama [1], Email [2], Kota [3])
sheet.getRange(i + 1, 2).setValue(newNama);
sheet.getRange(i + 1, 3).setValue(newEmail);
sheet.getRange(i + 1, 4).setValue(newKota);
// Jika password diisi/diubah, update juga password (Kolom 5 / index 4)
if (newPasswordPlain && newPasswordPlain.trim() !== "") {
var newPasswordHashed = hashPassword(newPasswordPlain);
sheet.getRange(i + 1, 5).setValue(newPasswordHashed);
}
return ContentService.createTextOutput(JSON.stringify({
status: "success",
message: "Profil berhasil diperbarui!",
nama: newNama,
email: newEmail,
kota: newKota
})).setMimeType(ContentService.MimeType.JSON);
}
}
return ContentService.createTextOutput(JSON.stringify({status: "failed", message: "Akun tidak ditemukan!"}))
.setMimeType(ContentService.MimeType.JSON);
}
- Deploy Skrip:
- Klik tombol Deploy di sudut kanan atas > New deployment (Deployment baru).
- Pilih jenis konfigurasi (Select type) Web app.
- Isi Deskripsi (bebas), tentukan Execute as ke Me (email Anda), dan pada bagian Who has access pilih Anyone (Siapa saja).
- Klik Deploy, berikan izin akses (Authorize access), lalu salin Web App URL yang dihasilkan. Simpan URL ini karena akan digunakan pada kode frontend blog Anda.
Langkah 2: Membuat Halaman Login
Buat halaman untuk pendaftaran dan login ke halaman member, ikuti langkah dibawah ini.
- Buat halaman baru di blog Anda (misalnya: Halaman login).
- Salin dan tempel kode dari File 2 di bawah ini ke dalam mode HTML editor postingan Anda:
<style>
.auth-container {
max-width: 400px;
margin: 20px auto;
background: #ffffff;
padding: 24px;
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
box-sizing: border-box;
}
.auth-container h2 {
text-align: center;
color: #333;
margin-bottom: 20px;
font-size: 24px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 6px;
color: #555;
font-weight: 600;
font-size: 14px;
}
.form-group input {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 14px;
box-sizing: border-box;
transition: border-color 0.3s;
}
.form-group input:focus {
border-color: #007bff;
outline: none;
}
.auth-btn {
width: 100%;
padding: 12px;
background: #007bff;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: background 0.3s;
}
.auth-btn:hover {
background: #0056b3;
}
.toggle-link {
text-align: center;
margin-top: 15px;
font-size: 14px;
color: #666;
}
.toggle-link a {
color: #007bff;
text-decoration: none;
font-weight: 600;
}
.toggle-link a:hover {
text-decoration: underline;
}
.alert-msg {
padding: 10px;
margin-bottom: 15px;
border-radius: 6px;
font-size: 14px;
text-align: center;
display: none;
}
.alert-success { background: #d4edda; color: #155724; }
.alert-error { background: #f8d7da; color: #721c24; }
</style>
<div class="auth-container">
<div id="alertBox" class="alert-msg"></div>
<!-- FORM LOGIN -->
<div id="login-section">
<h2>Login Member</h2>
<form id="loginForm" onsubmit="handleLogin(event)">
<div class="form-group">
<label>Email</label>
<input type="email" id="login-email" required placeholder="Masukkan email Anda">
</div>
<div class="form-group">
<label>Password</label>
<input type="password" id="login-password" required placeholder="Masukkan password">
</div>
<button type="submit" class="auth-btn" id="loginBtn">Masuk</button>
</form>
<div class="toggle-link">
Belum punya akun? <a href="#" onclick="switchForm('register')">Daftar sekarang</a>
</div>
</div>
<!-- FORM PENDAFTARAN -->
<div id="register-section" style="display: none;">
<h2>Daftar Akun Baru</h2>
<form id="regForm" onsubmit="handleRegister(event)">
<div class="form-group">
<label>Nama Lengkap</label>
<input type="text" id="reg-nama" required placeholder="Nama lengkap Anda">
</div>
<div class="form-group">
<label>Email</label>
<input type="email" id="reg-email" required placeholder="Email aktif">
</div>
<div class="form-group">
<label>Kota Asal</label>
<input type="text" id="reg-kota" required placeholder="Contoh: Jakarta">
</div>
<div class="form-group">
<label>Password</label>
<input type="password" id="reg-password" required placeholder="Buat password">
</div>
<button type="submit" class="auth-btn" id="regBtn">Daftar</button>
</form>
<div class="toggle-link">
Sudah punya akun? <a href="#" onclick="switchForm('login')">Login di sini</a>
</div>
</div>
</div>
<!-- Hidden iframe untuk menangani respon Apps Script tanpa error CORS -->
<iframe name="hidden_iframe" id="hidden_iframe" style="display:none;" onload="if(window.submitted) { handleResponse(); }"></iframe>
<script>
const SCRIPT_URL = "https://script.google.com/macros/s/AKfycbzWbZy9XV6VHXFZFFELsJHQxPPBEiX98nDMT7HXqd-m4AH0svxnVNB1RnD2jvh_VVXT/exec";
// Ganti URL di bawah dengan URL Halaman Member Area Anda yang sebenarnya (bukan halaman login)
const MEMBER_PAGE_URL = "https://trialloginmember.blogspot.com/p/member-area.html";
window.onload = function() {
let savedUser = localStorage.getItem("member_session");
if (savedUser) {
window.location.href = MEMBER_PAGE_URL;
}
}
function switchForm(type) {
hideAlert();
if (type === 'register') {
document.getElementById('login-section').style.display = 'none';
document.getElementById('register-section').style.display = 'block';
} else {
document.getElementById('register-section').style.display = 'none';
document.getElementById('login-section').style.display = 'block';
}
}
function showAlert(message, type) {
let alertBox = document.getElementById('alertBox');
alertBox.innerText = message;
alertBox.className = "alert-msg " + (type === 'success' ? 'alert-success' : 'alert-error');
alertBox.style.display = 'block';
}
function hideAlert() {
document.getElementById('alertBox').style.display = 'none';
}
// Fungsi Pendaftaran menggunakan metode aman iframe
function handleRegister(e) {
e.preventDefault();
let btn = document.getElementById('regBtn');
btn.innerText = "Memproses...";
btn.disabled = true;
let form = document.createElement('form');
form.action = SCRIPT_URL;
form.method = 'POST';
form.target = 'hidden_iframe';
let data = {
action: "register",
nama: document.getElementById('reg-nama').value,
email: document.getElementById('reg-email').value,
kota: document.getElementById('reg-kota').value,
password: document.getElementById('reg-password').value
};
for (let key in data) {
let input = document.createElement('input');
input.type = 'hidden';
input.name = key;
input.value = data[key];
form.appendChild(input);
}
document.body.appendChild(form);
window.submitted = true;
form.submit();
setTimeout(() => {
btn.innerText = "Daftar";
btn.disabled = false;
showAlert("Pendaftaran berhasil! Silakan login.", "success");
document.getElementById('regForm').reset();
setTimeout(() => switchForm('login'), 1500);
}, 1500);
}
// Fungsi Login menggunakan fetch dengan mode no-cors / alternatif aman
function handleLogin(e) {
e.preventDefault();
let btn = document.getElementById('loginBtn');
btn.innerText = "Memproses...";
btn.disabled = true;
let email = document.getElementById('login-email').value;
let password = document.getElementById('login-password').value;
let formData = new URLSearchParams();
formData.append("action", "login");
formData.append("email", email);
formData.append("password", password);
fetch(SCRIPT_URL, {
method: "POST",
body: formData
})
.then(res => res.json())
.then(data => {
btn.innerText = "Masuk";
btn.disabled = false;
if (data.status === "success") {
let userData = { nama: data.nama, email: data.email, kota: data.kota };
localStorage.setItem("member_session", JSON.stringify(userData));
window.location.href = MEMBER_PAGE_URL;
} else {
showAlert(data.message, "error");
}
})
.catch(err => {
// Mengatasi pembatasan CORS browser dengan validasi langsung via parameter GET jika POST terhalang
let checkUrl = `${SCRIPT_URL}?action=login&email=${encodeURIComponent(email)}&password=${encodeURIComponent(password)}`;
fetch(checkUrl)
.then(response => response.json())
.then(data => {
btn.innerText = "Masuk";
btn.disabled = false;
if (data.status === "success") {
let userData = { nama: data.nama, email: data.email, kota: data.kota };
localStorage.setItem("member_session", JSON.stringify(userData));
window.location.href = MEMBER_PAGE_URL;
} else {
showAlert(data.message, "error");
}
})
.catch(e => {
btn.innerText = "Masuk";
btn.disabled = false;
showAlert("Email atau Password salah, atau periksa koneksi.", "error");
});
});
}
function handleResponse() {
// Handler iframe post selesai
}
</script>
Langkah 3: Membuat Halaman Dashboard Utama Member Area
Setelah member berhasil login, mereka akan diarahkan ke halaman Member Area. Halaman ini berisi informasi profil, tombol edit profil, tombol keluar (logout), serta grid menu navigasi halaman eksklusif lengkap dengan fitur paginasi.
- Buat halaman baru lagi di blog Anda khusus untuk Member Area.
- Masukkan kode dari File 3 di bawah ini ke editor HTML halaman tersebut:
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Member Area</title>
<style>
/* Styling Loading Screen dengan Background Kuning */
#pageLoadingScreen {
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(255, 243, 205, 0.98);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 9999;
transition: opacity 0.4s ease;
}
.spinner {
width: 48px;
height: 48px;
border: 4px solid #ffeeba;
border-top: 4px solid #856404;
border-radius: 50%;
animation: spin 0.9s linear infinite;
margin-bottom: 16px;
}
.loading-text {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 15px;
color: #856404;
font-weight: 600;
letter-spacing: 0.3px;
text-align: center;
padding: 0 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.member-wrapper {
max-width: 900px;
margin: 20px auto;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
box-sizing: border-box;
padding: 0 15px;
}
#globalAlert {
padding: 12px 20px;
margin-bottom: 20px;
border-radius: 8px;
font-size: 14px;
text-align: center;
font-weight: 600;
display: none;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}
.member-topbar {
background: #ffffff;
padding: 12px 20px;
border-radius: 50px;
box-shadow: 0 4px 15px rgba(0,0,0,0.08);
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
border: 1px solid #eaeaea;
}
.member-info-mini {
display: flex;
align-items: center;
gap: 12px;
overflow: hidden;
}
.member-avatar {
width: 40px;
height: 40px;
background: #007bff;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 2px 6px rgba(0,123,255,0.3);
}
.member-avatar svg {
width: 22px;
height: 22px;
fill: currentColor;
}
.member-text {
overflow: hidden;
}
.member-text h4 {
margin: 0;
font-size: 15px;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.member-text p {
margin: 2px 0 0 0;
font-size: 12px;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.member-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
.btn-mini {
padding: 7px 14px;
border: none;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-edit-toggle { background: #007bff; color: white; }
.btn-edit-toggle:hover { background: #0056b3; }
.btn-logout { background: #fff5f5; color: #dc3545; }
.btn-logout:hover { background: #ffe3e3; }
.inline-edit-card {
background: #ffffff;
padding: 25px;
border-radius: 16px;
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
border: 1px solid #eaeaea;
margin-bottom: 25px;
display: none;
}
.inline-edit-card h3 {
margin-top: 0;
color: #333;
font-size: 18px;
margin-bottom: 15px;
border-bottom: 2px solid #f1f3f5;
padding-bottom: 10px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 6px;
color: #555;
font-weight: 600;
font-size: 13px;
}
.form-group input {
width: 100%;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 14px;
box-sizing: border-box;
}
.form-group input:focus {
border-color: #007bff;
outline: none;
}
.form-buttons {
display: flex;
gap: 10px;
margin-top: 20px;
}
.auth-btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
color: white;
}
.save-btn { background: #28a745; }
.save-btn:hover { background: #218838; }
.cancel-btn { background: #6c757d; }
.cancel-btn:hover { background: #5a6268; }
.main-content-area {
background: #ffffff;
padding: 30px;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0,0,0,0.04);
border: 1px solid #eaeaea;
}
.dashboard-header {
margin-bottom: 25px;
}
.dashboard-header h3 {
margin: 0 0 5px 0;
font-size: 20px;
color: #222;
}
.dashboard-header p {
margin: 0;
color: #666;
font-size: 14px;
}
.menu-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
min-height: 320px;
}
.menu-card {
background: #fff;
border: 1px solid #edf2f7;
border-radius: 14px;
padding: 22px;
text-decoration: none;
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
overflow: hidden;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0,0,0,0.02);
}
.menu-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(0,0,0,0.08);
border-color: transparent;
}
.theme-blue::before { background: linear-gradient(135deg, #007bff, #00d2ff); }
.theme-green::before { background: linear-gradient(135deg, #28a745, #a8ff78); }
.theme-orange::before { background: linear-gradient(135deg, #ff9966, #ff5e62); }
.theme-purple::before { background: linear-gradient(135deg, #8e2de2, #4a00e0); }
.theme-yellow::before { background: linear-gradient(135deg, #f7971e, #ffd200); }
.theme-red::before { background: linear-gradient(135deg, #eb3349, #f45c43); }
.menu-card::before {
content: '';
position: absolute;
top: 0; left: 0; width: 100%; height: 5px;
}
.menu-icon-wrapper {
width: 50px;
height: 50px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
}
.theme-blue .menu-icon-wrapper { background: #e8f4fd; color: #007bff; }
.theme-green .menu-icon-wrapper { background: #eef9f0; color: #28a745; }
.theme-orange .menu-icon-wrapper { background: #fff3ee; color: #ff6633; }
.theme-purple .menu-icon-wrapper { background: #f3eafd; color: #8e2de2; }
.theme-yellow .menu-icon-wrapper { background: #fffbec; color: #f7971e; }
.theme-red .menu-icon-wrapper { background: #fef1f2; color: #eb3349; }
.menu-icon-wrapper svg {
width: 26px;
height: 26px;
fill: currentColor;
}
.menu-info h4 {
margin: 0 0 6px 0;
font-size: 16px;
color: #333;
font-weight: 700;
}
.menu-info p {
margin: 0;
font-size: 13px;
color: #666;
line-height: 1.4;
}
.menu-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 20px;
padding-top: 12px;
border-top: 1px solid #f1f3f5;
font-size: 13px;
font-weight: 600;
color: #007bff;
}
.menu-card:hover .menu-footer span {
text-decoration: underline;
}
.menu-footer svg {
width: 16px;
height: 16px;
fill: currentColor;
transition: transform 0.2s;
}
.menu-card:hover .menu-footer svg {
transform: translateX(4px);
}
.pagination-container {
display: flex;
justify-content: center;
align-items: center;
gap: 6px;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #eaeaea;
}
.page-btn {
min-width: 36px;
height: 36px;
padding: 0 10px;
border: 1px solid #dee2e6;
background: #ffffff;
color: #007bff;
font-size: 14px;
font-weight: 600;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.page-btn:hover {
background: #f1f3f5;
border-color: #ced4da;
}
.page-btn.active {
background: #007bff;
color: #ffffff;
border-color: #007bff;
box-shadow: 0 4px 10px rgba(0,123,255,0.3);
}
.page-btn:disabled {
color: #adb5bd;
border-color: #e9ecef;
background: #f8f9fa;
cursor: not-allowed;
box-shadow: none;
}
</style>
</head>
<body>
<!-- Layar Animasi Loading Background Kuning -->
<div id="pageLoadingScreen">
<div class="spinner"></div>
<div class="loading-text" id="loadingText">Memuat data menu member...</div>
</div>
<div class="member-wrapper">
<div id="globalAlert"></div>
<div class="member-topbar">
<div class="member-info-mini">
<div class="member-avatar">
<svg viewBox="0 0 24 24">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
</svg>
</div>
<div class="member-text">
<h4 id="bar-nama">Memuat...</h4>
<p id="bar-email">- | <span id="bar-kota">-</span></p>
</div>
</div>
<div class="member-actions">
<button class="btn-mini btn-edit-toggle" id="toggleEditBtn" onclick="toggleEditForm()">Edit Profil</button>
<button class="btn-mini btn-logout" onclick="memberLogout()">Keluar</button>
</div>
</div>
<div class="inline-edit-card" id="editCard">
<h3>Form Perbarui Profil Akun</h3>
<form id="editForm" onsubmit="handleUpdate(event)">
<div class="form-group">
<label>Nama Lengkap</label>
<input type="text" id="edit-nama" required>
</div>
<div class="form-group">
<label>Email</label>
<input type="email" id="edit-email" required>
</div>
<div class="form-group">
<label>Kota Asal</label>
<input type="text" id="edit-kota" required>
</div>
<div class="form-group">
<label>Password Baru <small style="color: #777; font-weight: normal;">(Opsional)</small></label>
<input type="password" id="edit-password" placeholder="Kosongkan jika tidak ingin ganti">
</div>
<div class="form-buttons">
<button type="submit" id="saveBtn" class="auth-btn save-btn">Simpan Perubahan</button>
<button type="button" class="auth-btn cancel-btn" onclick="closeEditForm()">Batal</button>
</div>
</form>
</div>
<div class="main-content-area">
<div class="dashboard-header">
<h3>Menu Navigasi Member</h3>
<p>Silakan pilih menu di bawah ini untuk mengakses halaman fitur yang tersedia.</p>
</div>
<div class="menu-grid" id="menuGridContainer"></div>
<div class="pagination-container" id="paginationContainer"></div>
</div>
</div>
<iframe name="silent_frame" id="silent_frame" style="display:none;"></iframe>
<script>
const LOGIN_PAGE_URL = "https://trialloginmember.blogspot.com/p/form-login.html";
const SCRIPT_URL = "https://script.google.com/macros/s/AKfycbzWbZy9XV6VHXFZFFELsJHQxPPBEiX98nDMT7HXqd-m4AH0svxnVNB1RnD2jvh_VVXT/exec";
let currentUser = null;
let isEditingOpen = false;
const menuList = [
{ title: "Halaman 1", desc: "Dashboard ringkasan statistik dan informasi penting akun Anda.", url: "https://trialloginmember.blogspot.com/p/halaman-member-1.html", theme: "theme-blue", icon: '<path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/>' },
{ title: "Halaman 2", desc: "Akses arsip dokumen, file unduhan, dan panduan eksklusif.", url: "https://trialloginmember.blogspot.com/p/halaman-2.html", theme: "theme-green", icon: '<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/>' },
{ title: "Halaman 3", desc: "Temukan jawaban cepat atau tanyakan kendala seputar layanan.", url: "https://trialloginmember.blogspot.com/p/halaman-3.html", theme: "theme-orange", icon: '<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H7c0-2.76 2.24-5 5-5s5 2.24 5 5c0 1.02-.42 1.95-1.07 2.75z"/>' },
{ title: "Halaman 4", desc: "Baca informasi terbaru, update artikel, dan pengumuman penting.", url: "https://trialloginmember.blogspot.com/p/halaman-4.html", theme: "theme-purple", icon: '<path d="M18 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM6 4h5v8l-2.5-1.5L6 12V4z"/>' },
{ title: "Halaman 5", desc: "Kelola pengaturan sandi, verifikasi, dan riwayat aktivitas login.", url: "https://trialloginmember.blogspot.com/p/halaman-5.html", theme: "theme-yellow", icon: '<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/>' },
{ title: "Halaman 6", desc: "Fitur tambahan eksklusif dan alat bantu penunjang produktivitas.", url: "https://trialloginmember.blogspot.com/p/halaman-6.html", theme: "theme-red", icon: '<path d="M20 6h-4V4c0-1.1-.9-2-2-2h-4c-1.1 0-2 .9-2 2v2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zM10 4h4v2h-4V4zm10 16H4V8h16v12z"/>' },
{ title: "Halaman 7", desc: "Pusat unduhan berkas panduan teknis dan lembar kerja.", url: "https://trialloginmember.blogspot.com/p/halaman-7.html", theme: "theme-blue", icon: '<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>' },
{ title: "Halaman 8", desc: "Galeri foto kegiatan, dokumentasi proyek, serta banner media.", url: "https://trialloginmember.blogspot.com/p/halaman-8.html", theme: "theme-green", icon: '<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>' },
{ title: "Halaman 9", desc: "Layanan konsultasi langsung bersama tim ahli pendukung.", url: "https://trialloginmember.blogspot.com/p/halaman-9.html", theme: "theme-orange", icon: '<path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM6 9h12v2H6V9zm8 5H6v-2h8v2zm4-6H6V6h12v2z"/>' },
{ title: "Halaman 10", desc: "Jadwal kegiatan berkala dan kalender agenda operasional.", url: "https://trialloginmember.blogspot.com/p/halaman-10.html", theme: "theme-purple", icon: '<path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11z"/>' },
{ title: "Halaman 11", desc: "Pengaturan preferensi tema, tampilan, dan notifikasi akun.", url: "https://trialloginmember.blogspot.com/p/halaman-11.html", theme: "theme-yellow", icon: '<path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c-.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/>' },
{ title: "Halaman 12", desc: "Arsip laporan lengkap, rekapitulasi data, dan unduh sertifikat.", url: "https://trialloginmember.blogspot.com/p/halaman-12.html", theme: "theme-red", icon: '<path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/>' }
];
const itemsPerPage = 6;
let currentPage = 1;
// Eksekusi Aman Menggunakan window.onload untuk Menjamin Seluruh Elemen Telah Selesai Dimuat
window.addEventListener('load', () => {
try {
let sessionData = localStorage.getItem("member_session");
if (!sessionData) {
// Jika sesi kosong, arahkan langsung ke halaman login
window.location.href = LOGIN_PAGE_URL;
return;
}
currentUser = JSON.parse(sessionData);
renderBar(currentUser);
renderMenuPage(currentPage);
} catch (err) {
console.error("Terjadi kesalahan saat memuat sesi:", err);
// Fallback jika localStorage rusak/korup
localStorage.removeItem("member_session");
window.location.href = LOGIN_PAGE_URL;
return;
}
// Hilangkan Layar Loading dengan Aman
let loader = document.getElementById('pageLoadingScreen');
if (loader) {
loader.style.opacity = '0';
setTimeout(() => {
loader.style.display = 'none';
}, 400);
}
});
function renderBar(user) {
const namaEl = document.getElementById('bar-nama');
const emailEl = document.getElementById('bar-email');
const kotaEl = document.getElementById('bar-kota');
if (namaEl) namaEl.innerText = user.nama || "Member";
if (emailEl) emailEl.innerText = user.email || "-";
if (kotaEl) kotaEl.innerText = user.kota || "-";
}
function renderMenuPage(page) {
currentPage = page;
const gridContainer = document.getElementById('menuGridContainer');
const paginationContainer = document.getElementById('paginationContainer');
if (!gridContainer) return;
gridContainer.innerHTML = "";
const startIndex = (page - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const currentMenus = menuList.slice(startIndex, endIndex);
currentMenus.forEach(menu => {
let card = document.createElement('a');
card.href = menu.url;
card.className = `menu-card ${menu.theme}`;
card.innerHTML = `
<div>
<div class="menu-icon-wrapper">
<svg viewBox="0 0 24 24">${menu.icon}</svg>
</div>
<div class="menu-info">
<h4>${menu.title}</h4>
<p>${menu.desc}</p>
</div>
</div>
<div class="menu-footer">
<span>Buka Menu</span>
<svg viewBox="0 0 24 24"><path d="M5 13h11.86l-5.43 5.43 1.42 1.42L21.71 12l-7.86-7.86-1.42 1.42L16.86 11H5v2z"/></svg>
</div>
`;
gridContainer.appendChild(card);
});
renderPaginationControls(paginationContainer);
}
function renderPaginationControls(container) {
if (!container) return;
container.innerHTML = "";
const totalPages = Math.ceil(menuList.length / itemsPerPage);
if (totalPages <= 1) return;
let prevBtn = document.createElement('button');
prevBtn.className = 'page-btn';
prevBtn.innerHTML = '«';
prevBtn.disabled = currentPage === 1;
prevBtn.onclick = () => { if (currentPage > 1) renderMenuPage(currentPage - 1); };
container.appendChild(prevBtn);
for (let i = 1; i <= totalPages; i++) {
let pageBtn = document.createElement('button');
pageBtn.className = `page-btn ${i === currentPage ? 'active' : ''}`;
pageBtn.innerText = i;
pageBtn.onclick = () => renderMenuPage(i);
container.appendChild(pageBtn);
}
let nextBtn = document.createElement('button');
nextBtn.className = 'page-btn';
nextBtn.innerHTML = '»';
nextBtn.disabled = currentPage === totalPages;
nextBtn.onclick = () => { if (currentPage < totalPages) renderMenuPage(currentPage + 1); };
container.appendChild(nextBtn);
}
function toggleEditForm() {
if (isEditingOpen) {
closeEditForm();
} else {
openEditForm();
}
}
function openEditForm() {
let card = document.getElementById('editCard');
let btn = document.getElementById('toggleEditBtn');
document.getElementById('edit-nama').value = currentUser.nama;
document.getElementById('edit-email').value = currentUser.email;
document.getElementById('edit-kota').value = currentUser.kota;
document.getElementById('edit-password').value = "";
card.style.display = 'block';
btn.innerText = "Batal";
btn.style.background = "#6c757d";
isEditingOpen = true;
card.scrollIntoView({ behavior: 'smooth' });
}
function closeEditForm() {
let card = document.getElementById('editCard');
let btn = document.getElementById('toggleEditBtn');
card.style.display = 'none';
btn.innerText = "Edit Profil";
btn.style.background = "#007bff";
isEditingOpen = false;
}
function handleUpdate(e) {
e.preventDefault();
let saveBtn = document.getElementById('saveBtn');
saveBtn.innerText = "Menyimpan...";
saveBtn.disabled = true;
let newNama = document.getElementById('edit-nama').value;
let newEmail = document.getElementById('edit-email').value;
let newKota = document.getElementById('edit-kota').value;
let newPassword = document.getElementById('edit-password').value;
let form = document.createElement('form');
form.action = SCRIPT_URL;
form.method = 'POST';
form.target = 'silent_frame';
let payloads = {
action: "update",
oldEmail: currentUser.email,
nama: newNama,
email: newEmail,
kota: newKota,
password: newPassword
};
for (let key in payloads) {
let input = document.createElement('input');
input.type = 'hidden';
input.name = key;
input.value = payloads[key];
form.appendChild(input);
}
document.body.appendChild(form);
form.submit();
document.body.removeChild(form);
currentUser = { nama: newNama, email: newEmail, kota: newKota };
localStorage.setItem("member_session", JSON.stringify(currentUser));
setTimeout(() => {
location.reload();
}, 800);
}
function memberLogout() {
localStorage.removeItem("member_session");
window.location.href = LOGIN_PAGE_URL;
}
</script>
</body>
</html>
Langkah 4: Memasang Proteksi pada Halaman Khusus Member
Jika Anda ingin mengamankan sebuah artikel atau halaman agar hanya bisa dilihat oleh member yang sudah login, Anda bisa memasang kode proteksi ini.
- Buat halaman baru di blog Anda (misalnya: Halaman Konten Eksklusif Member).
- Salin dan tempel kode dari File 4 di bawah ini ke dalam mode HTML editor postingan (letakkan dipaling bawah kode postingan Anda):
<!-- ================================================================= -->
<!-- KODE PROTEKSI HALAMAN MEMBER (PASTE DI BAWAH KONTEN/ARTIKEL ANDA) -->
<!-- ================================================================= -->
<!-- Tampilan Layar Peringatan & Tombol Login -->
<div id="protectionScreen">
<div class="protection-card">
<div class="protection-icon">
<svg viewBox="0 0 24 24">
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/>
</svg>
</div>
<h3>Akses Terbatas!</h3>
<p>Silakan login terlebih dahulu untuk dapat mengakses halaman eksklusif member ini.</p>
<a href="https://trialloginmember.blogspot.com/p/form-login.html" class="btn-login-redirect">Login Sekarang</a>
</div>
</div>
<!-- Styling & Skrip Pendukung Proteksi -->
<style>
#protectionScreen {
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(255, 243, 205, 0.98);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 9999;
padding: 20px;
box-sizing: border-box;
text-align: center;
}
.protection-card {
background: #ffffff;
padding: 35px 30px;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0,0,0,0.08);
max-width: 400px;
width: 100%;
border: 1px solid #ffeeba;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.protection-icon {
width: 60px;
height: 60px;
background: #fff3cd;
color: #856404;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 20px auto;
}
.protection-icon svg {
width: 30px;
height: 30px;
fill: currentColor;
}
.protection-card h3 {
margin: 0 0 10px 0;
color: #333;
font-size: 20px;
}
.protection-card p {
margin: 0 0 25px 0;
color: #666;
font-size: 14px;
line-height: 1.5;
}
.btn-login-redirect {
display: inline-block;
background: #007bff;
color: white;
padding: 12px 25px;
border-radius: 30px;
text-decoration: none;
font-weight: bold;
font-size: 14px;
box-shadow: 0 4px 15px rgba(0,123,255,0.3);
transition: background 0.3s, transform 0.2s;
}
.btn-login-redirect:hover {
background: #0056b3;
transform: translateY(-2px);
}
</style>
<script>
window.addEventListener('DOMContentLoaded', () => {
let sessionData = localStorage.getItem("member_session");
let contentElement = document.getElementById('protectedContent');
if (sessionData) {
// Jika sesi valid, sembunyikan layar peringatan
document.getElementById('protectionScreen').style.display = 'none';
if (contentElement) {
contentElement.style.display = 'block';
}
} else {
// Jika belum login, pastikan konten utama disembunyikan
if (contentElement) {
contentElement.style.display = 'none';
}
}
});
</script>
Kesimpulan
Dengan mengikuti langkah-langkah di atas, Anda kini memiliki sistem manajemen member mandiri yang terhubung langsung ke Google Spreadsheet tanpa memerlukan server hosting berbayar atau konfigurasi database yang rumit. Selamat mencoba dan semoga artikel panduan ini bermanfaat untuk blog Anda!
Posting Komentar untuk "Cara Membuat Sistem Login Member Area Terintegrasi Google Spreadsheet (Tanpa Database Server)"