Files
UAS_WEBGIS_MuhammadFarras_D…/WebGIS_PovertyMap/login.html
T
2026-06-09 20:22:09 +07:00

846 lines
25 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Login WebGIS Pemetaan Kemiskinan Berbasis Partisipasi Rumah Ibadah">
<title>Masuk WebGIS Poverty Mapping</title>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap"
rel="stylesheet">
<style>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg: #0a0e1a;
--panel: #0f1422;
--card: #141928;
--border: rgba(255, 255, 255, .07);
--blue: #4f7cff;
--blue-dim: rgba(79, 124, 255, .12);
--green: #22d3a5;
--orange: #ff8c42;
--purple: #9b6dff;
--text: #e8edf5;
--muted: #5c6880;
--sub: #8a96b0;
}
body {
font-family: 'Plus Jakarta Sans', sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: grid;
grid-template-columns: 1fr 480px;
}
/* ── LEFT PANEL ── */
.left-panel {
position: relative;
overflow: hidden;
background: #080c18;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 48px;
}
.map-bg {
position: absolute;
inset: 0;
background:
radial-gradient(ellipse 70% 60% at 30% 40%, rgba(79, 124, 255, .18) 0%, transparent 65%),
radial-gradient(ellipse 50% 70% at 70% 70%, rgba(34, 211, 165, .1) 0%, transparent 60%),
radial-gradient(ellipse 40% 40% at 60% 20%, rgba(155, 109, 255, .08) 0%, transparent 55%);
}
/* Animated grid lines */
.grid-lines {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(255, 255, 255, .025) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, .025) 1px, transparent 1px);
background-size: 60px 60px;
animation: gridShift 20s linear infinite;
}
@keyframes gridShift {
from {
background-position: 0 0;
}
to {
background-position: 60px 60px;
}
}
/* Floating map dots */
.dot {
position: absolute;
border-radius: 50%;
pointer-events: none;
}
.dot-1 {
width: 12px;
height: 12px;
background: var(--blue);
box-shadow: 0 0 20px var(--blue), 0 0 40px rgba(79, 124, 255, .4);
top: 32%;
left: 28%;
animation: pulseDot 3s ease-in-out infinite;
}
.dot-2 {
width: 8px;
height: 8px;
background: var(--green);
box-shadow: 0 0 15px var(--green);
top: 55%;
left: 55%;
animation: pulseDot 3s ease-in-out infinite 1s;
}
.dot-3 {
width: 10px;
height: 10px;
background: var(--orange);
box-shadow: 0 0 15px var(--orange);
top: 22%;
left: 60%;
animation: pulseDot 3s ease-in-out infinite 2s;
}
.dot-4 {
width: 7px;
height: 7px;
background: var(--purple);
box-shadow: 0 0 12px var(--purple);
top: 68%;
left: 35%;
animation: pulseDot 3s ease-in-out infinite .5s;
}
/* Connecting lines between dots */
.conn-lines {
position: absolute;
inset: 0;
overflow: hidden;
}
.conn-lines svg {
width: 100%;
height: 100%;
}
@keyframes pulseDot {
0%,
100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.4);
opacity: .6;
}
}
/* Ripple rings on dots */
.dot::after {
content: '';
position: absolute;
inset: -8px;
border-radius: 50%;
border: 1.5px solid currentColor;
opacity: 0;
animation: ripple 3s ease-in-out infinite;
}
.dot-1::after {
color: var(--blue);
}
.dot-2::after {
color: var(--green);
animation-delay: 1s;
}
.dot-3::after {
color: var(--orange);
animation-delay: 2s;
}
.dot-4::after {
color: var(--purple);
animation-delay: .5s;
}
@keyframes ripple {
0% {
inset: -4px;
opacity: .7;
}
100% {
inset: -24px;
opacity: 0;
}
}
.left-content {
position: relative;
z-index: 2;
}
.left-tag {
display: inline-flex;
align-items: center;
gap: 7px;
background: rgba(79, 124, 255, .15);
border: 1px solid rgba(79, 124, 255, .3);
border-radius: 20px;
padding: 5px 14px;
font-size: 11px;
font-weight: 600;
color: #7ba4ff;
letter-spacing: .5px;
text-transform: uppercase;
margin-bottom: 20px;
}
.left-tag-dot {
width: 6px;
height: 6px;
background: var(--blue);
border-radius: 50%;
box-shadow: 0 0 8px var(--blue);
animation: pulseDot 2s ease-in-out infinite;
}
.left-title {
font-size: clamp(28px, 3.5vw, 42px);
font-weight: 800;
line-height: 1.15;
letter-spacing: -.5px;
color: #fff;
margin-bottom: 16px;
}
.left-title em {
font-style: normal;
background: linear-gradient(90deg, var(--blue), var(--green));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.left-desc {
font-size: 14px;
color: var(--sub);
line-height: 1.7;
max-width: 400px;
margin-bottom: 32px;
}
.stat-row {
display: flex;
gap: 20px;
}
.stat {
display: flex;
flex-direction: column;
gap: 2px;
}
.stat-val {
font-size: 24px;
font-weight: 800;
color: #fff;
}
.stat-lbl {
font-size: 11px;
color: var(--muted);
font-weight: 500;
}
.stat-sep {
width: 1px;
background: var(--border);
align-self: stretch;
}
/* ── RIGHT PANEL ── */
.right-panel {
background: var(--panel);
border-left: 1px solid var(--border);
display: flex;
flex-direction: column;
justify-content: center;
padding: 48px 40px;
overflow-y: auto;
}
.logo-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 40px;
}
.logo-icon {
width: 44px;
height: 44px;
background: linear-gradient(135deg, #1a3a8f, var(--blue));
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
box-shadow: 0 6px 20px rgba(79, 124, 255, .3);
flex-shrink: 0;
}
.logo-text {
display: flex;
flex-direction: column;
}
.logo-name {
font-size: 14px;
font-weight: 800;
color: var(--text);
letter-spacing: -.2px;
}
.logo-sub {
font-size: 11px;
color: var(--muted);
margin-top: 1px;
}
.form-title {
font-size: 26px;
font-weight: 800;
color: var(--text);
letter-spacing: -.4px;
margin-bottom: 6px;
}
.form-sub {
font-size: 13px;
color: var(--sub);
margin-bottom: 32px;
}
/* Role tabs */
.role-tabs {
display: flex;
background: var(--card);
border: 1px solid var(--border);
border-radius: 10px;
padding: 4px;
gap: 4px;
margin-bottom: 28px;
}
.role-tab {
flex: 1;
padding: 8px 6px;
border: none;
border-radius: 7px;
background: transparent;
color: var(--muted);
font-family: inherit;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all .2s;
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
}
.role-tab-icon {
font-size: 18px;
}
.role-tab-name {
font-size: 11px;
}
.role-tab:hover {
color: var(--text);
background: rgba(255, 255, 255, .04);
}
.role-tab.active {
background: var(--blue-dim);
color: var(--blue);
border: 1px solid rgba(79, 124, 255, .25);
}
.role-tab.active[data-role="pengurus"] {
color: var(--purple);
background: rgba(155, 109, 255, .1);
border-color: rgba(155, 109, 255, .25);
}
.role-tab.active[data-role="walikota"] {
color: var(--orange);
background: rgba(255, 140, 66, .1);
border-color: rgba(255, 140, 66, .25);
}
/* Fields */
.field {
margin-bottom: 18px;
}
.field-label {
display: block;
font-size: 12px;
font-weight: 600;
color: var(--sub);
margin-bottom: 7px;
letter-spacing: .3px;
}
.field-wrap {
position: relative;
}
.field-icon {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
width: 16px;
text-align: center;
color: var(--muted);
font-size: 14px;
pointer-events: none;
}
.field-input {
width: 100%;
padding: 12px 14px 12px 42px;
background: var(--card);
border: 1px solid var(--border);
border-radius: 10px;
color: var(--text);
font-family: inherit;
font-size: 13.5px;
outline: none;
transition: border-color .2s, box-shadow .2s;
}
.field-input:focus {
border-color: rgba(79, 124, 255, .5);
box-shadow: 0 0 0 3px rgba(79, 124, 255, .1);
}
.field-input::placeholder {
color: var(--muted);
font-size: 13px;
}
.pw-toggle {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--muted);
font-size: 14px;
cursor: pointer;
padding: 4px;
transition: color .2s;
}
.pw-toggle:hover {
color: var(--sub);
}
/* Error */
.error-msg {
display: none;
align-items: center;
gap: 8px;
background: rgba(239, 68, 68, .08);
border: 1px solid rgba(239, 68, 68, .2);
color: #fca5a5;
border-radius: 8px;
padding: 10px 14px;
font-size: 12.5px;
margin-bottom: 18px;
animation: shake .3s ease;
}
.error-msg.show {
display: flex;
}
@keyframes shake {
0%,
100% {
transform: translateX(0);
}
25% {
transform: translateX(-5px);
}
75% {
transform: translateX(5px);
}
}
/* Login button */
.btn-masuk {
width: 100%;
padding: 14px;
background: var(--blue);
color: #fff;
border: none;
border-radius: 10px;
font-family: inherit;
font-size: 14px;
font-weight: 700;
cursor: pointer;
transition: all .25s;
box-shadow: 0 4px 20px rgba(79, 124, 255, .35);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-bottom: 24px;
}
.btn-masuk:hover {
background: #6b91ff;
transform: translateY(-1px);
box-shadow: 0 6px 28px rgba(79, 124, 255, .45);
}
.btn-masuk:active {
transform: translateY(0);
}
.btn-masuk.loading {
opacity: .7;
pointer-events: none;
}
/* Quick fill */
.quick-fill-label {
font-size: 11px;
font-weight: 600;
color: var(--muted);
text-transform: uppercase;
letter-spacing: .6px;
margin-bottom: 10px;
}
.quick-row {
display: flex;
gap: 8px;
}
.quick-btn {
flex: 1;
padding: 9px 8px;
background: var(--card);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--sub);
font-family: inherit;
font-size: 11px;
font-weight: 600;
cursor: pointer;
transition: all .2s;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.quick-btn:hover {
background: rgba(255, 255, 255, .05);
border-color: rgba(255, 255, 255, .15);
color: var(--text);
}
.footer-note {
text-align: center;
margin-top: 28px;
font-size: 11px;
color: var(--muted);
}
/* Responsive */
@media (max-width: 768px) {
body {
grid-template-columns: 1fr;
}
.left-panel {
display: none;
}
.right-panel {
padding: 36px 24px;
}
}
</style>
</head>
<body>
<!-- LEFT: Map Visual Panel -->
<div class="left-panel">
<div class="map-bg"></div>
<div class="grid-lines"></div>
<!-- Animated map dots -->
<div class="dot dot-1"></div>
<div class="dot dot-2"></div>
<div class="dot dot-3"></div>
<div class="dot dot-4"></div>
<!-- SVG connecting lines -->
<div class="conn-lines">
<svg viewBox="0 0 100 100" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
<line x1="28" y1="32" x2="55" y2="55" stroke="rgba(79,124,255,.2)" stroke-width=".3"
stroke-dasharray="2 2" />
<line x1="55" y1="55" x2="60" y2="22" stroke="rgba(34,211,165,.15)" stroke-width=".3"
stroke-dasharray="2 2" />
<line x1="28" y1="32" x2="35" y2="68" stroke="rgba(155,109,255,.15)" stroke-width=".3"
stroke-dasharray="2 2" />
<line x1="35" y1="68" x2="55" y2="55" stroke="rgba(255,140,66,.12)" stroke-width=".3"
stroke-dasharray="2 2" />
</svg>
</div>
<div class="left-content">
<div class="left-tag">
<div class="left-tag-dot"></div>
Sistem Aktif
</div>
<h1 class="left-title">
Peta Kemiskinan<br>Berbasis <em>Rumah Ibadah</em>
</h1>
<p class="left-desc">
Platform pemetaan partisipatif yang menghubungkan data kemiskinan dengan jaringan rumah ibadah untuk
perencanaan program yang lebih tepat sasaran.
</p>
<div class="stat-row">
<div class="stat">
<span class="stat-val" style="color:var(--blue)">3</span>
<span class="stat-lbl">Jenis Peran</span>
</div>
<div class="stat-sep"></div>
<div class="stat">
<span class="stat-val" style="color:var(--green)">6+</span>
<span class="stat-lbl">Jenis Rumah Ibadah</span>
</div>
<div class="stat-sep"></div>
<div class="stat">
<span class="stat-val" style="color:var(--orange)">GIS</span>
<span class="stat-lbl">Berbasis Peta</span>
</div>
</div>
</div>
</div>
<!-- RIGHT: Login Panel -->
<div class="right-panel">
<div class="logo-row">
<div class="logo-icon">🗺️</div>
<div class="logo-text">
<span class="logo-name">WebGIS Poverty Mapping</span>
<span class="logo-sub">Sistem Informasi Geografis</span>
</div>
</div>
<h2 class="form-title">Masuk ke Sistem</h2>
<p class="form-sub">Pilih peran, lalu masukkan kredensial Anda.</p>
<!-- Role Tabs -->
<div class="role-tabs" id="role-tabs">
<button class="role-tab" id="tab-admin" data-role="admin" onclick="pilihRole('admin')">
<span class="role-tab-icon">🛡️</span>
<span class="role-tab-name">Admin</span>
</button>
<button class="role-tab" id="tab-pengurus" data-role="pengurus" onclick="pilihRole('pengurus')">
<span class="role-tab-icon">🕌</span>
<span class="role-tab-name">Pengurus</span>
</button>
<button class="role-tab" id="tab-walikota" data-role="walikota" onclick="pilihRole('walikota')">
<span class="role-tab-icon">👑</span>
<span class="role-tab-name">Walikota</span>
</button>
</div>
<!-- Error -->
<div class="error-msg" id="error-msg">
<span>⚠️</span>
<span id="error-text">Username atau password salah.</span>
</div>
<!-- Form -->
<form id="login-form" onsubmit="handleLogin(event)" novalidate>
<div class="field">
<label class="field-label" for="inp-username">Username</label>
<div class="field-wrap">
<span class="field-icon">👤</span>
<input class="field-input" type="text" id="inp-username" placeholder="Masukkan username"
autocomplete="username" required>
</div>
</div>
<div class="field">
<label class="field-label" for="inp-password">Password</label>
<div class="field-wrap">
<span class="field-icon">🔑</span>
<input class="field-input" type="password" id="inp-password" placeholder="Masukkan password"
autocomplete="current-password" required>
<button type="button" class="pw-toggle" id="pw-toggle" onclick="togglePw()">👁️</button>
</div>
</div>
<button type="submit" class="btn-masuk" id="btn-masuk">
<span id="btn-text">Masuk</span>
<span id="btn-arrow"></span>
</button>
</form>
<div style="margin-bottom: 28px;">
<a href="index.html" style="display:flex; justify-content:center; align-items:center; gap:8px; padding:14px; background:rgba(255,255,255,0.03); color:var(--text); border:1px solid var(--border); border-radius:10px; text-decoration:none; font-size:13.5px; font-weight:600; transition:all 0.25s;" onmouseover="this.style.background='rgba(255,255,255,0.08)'" onmouseout="this.style.background='rgba(255,255,255,0.03)'">
<span>🗺️ Lihat Peta & Lapor (Akses Warga)</span>
</a>
</div>
<!-- Quick Fill -->
<div class="quick-fill-label">Akun Demo — Klik untuk Isi Otomatis</div>
<div class="quick-row">
<button class="quick-btn" onclick="isiDemo('admin','password','admin')">🛡️ Admin</button>
<button class="quick-btn" onclick="isiDemo('pengurus','password','pengurus')">🕌 Pengurus</button>
<button class="quick-btn" onclick="isiDemo('walikota','password','walikota')">👑 Walikota</button>
</div>
<div class="footer-note">WebGIS Poverty Mapping · 2026</div>
</div>
<script>
let roleSelected = null;
function pilihRole(role) {
roleSelected = role;
document.querySelectorAll('.role-tab').forEach(t => t.classList.remove('active'));
document.getElementById('tab-' + role).classList.add('active');
}
function isiDemo(user, pass, role) {
document.getElementById('inp-username').value = user;
document.getElementById('inp-password').value = pass;
pilihRole(role);
sembunyiError();
}
function togglePw() {
const inp = document.getElementById('inp-password');
const btn = document.getElementById('pw-toggle');
inp.type = inp.type === 'password' ? 'text' : 'password';
btn.textContent = inp.type === 'password' ? '👁️' : '🙈';
}
function tampilError(msg) {
const el = document.getElementById('error-msg');
document.getElementById('error-text').textContent = msg;
el.classList.add('show');
el.style.animation = 'none';
el.offsetHeight;
el.style.animation = '';
}
function sembunyiError() {
document.getElementById('error-msg').classList.remove('show');
}
async function handleLogin(e) {
e.preventDefault();
sembunyiError();
const username = document.getElementById('inp-username').value.trim();
const password = document.getElementById('inp-password').value;
if (!username) { tampilError('Username wajib diisi.'); return; }
if (!password) { tampilError('Password wajib diisi.'); return; }
if (!roleSelected) { tampilError('Pilih peran terlebih dahulu.'); return; }
const btn = document.getElementById('btn-masuk');
const btnText = document.getElementById('btn-text');
const btnArrow = document.getElementById('btn-arrow');
btn.classList.add('loading');
btnText.textContent = 'Memeriksa...';
btnArrow.textContent = '⏳';
try {
const res = await fetch('api_auth.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password, role: roleSelected })
});
const data = await res.json();
if (data.success) {
sessionStorage.setItem('webgis_user', JSON.stringify({
id: data.user.id,
username: data.user.username,
nama: data.user.nama,
role: data.user.role
}));
btnText.textContent = 'Berhasil!';
btnArrow.textContent = '✓';
btn.style.background = '#22d3a5';
const target = data.user.role === 'walikota' ? 'dashboard_walikota.html' : 'index.html';
setTimeout(() => { window.location.href = target; }, 500);
} else {
tampilError(data.message || 'Username, password, atau peran tidak sesuai.');
btn.classList.remove('loading');
btnText.textContent = 'Masuk';
btnArrow.textContent = '→';
}
} catch {
tampilError('Gagal terhubung ke server. Pastikan Apache & MySQL aktif.');
btn.classList.remove('loading');
btnText.textContent = 'Masuk';
btnArrow.textContent = '→';
}
}
// Handle logout redirect
if (new URLSearchParams(location.search).get('logout')) {
sessionStorage.removeItem('webgis_user');
}
</script>
</body>
</html>