commit fb3bcc656508f0e2b2b1a1726751619262d5748f Author: Medicone1 Date: Fri Aug 8 15:16:48 2025 +0700 first commit diff --git a/admin.html b/admin.html new file mode 100644 index 0000000..eeae48b --- /dev/null +++ b/admin.html @@ -0,0 +1,155 @@ + + + + + + Dashboard Admin - Monitoring Parkir + + + + + + +
+
+

Dashboard Admin - Monitoring Parkir

+
+ +
+
+

Status Parkir Saat Ini

+
+ Logo Kendaraan +

Parkir Slot 1

+

Tersedia

+
+
+ Logo Kendaraan +

Parkir Slot 2

+

Tersedia

+
+
+ Logo Kendaraan +

Parkir Slot 3

+

Tersedia

+
+
+
+

Riwayat Parkir Realtime

+
+
+ πŸ”„ Memuat riwayat parkir... +
+
+
+
+
+ + \ No newline at end of file diff --git a/images/Denah2.png b/images/Denah2.png new file mode 100644 index 0000000..8d7fab4 Binary files /dev/null and b/images/Denah2.png differ diff --git a/images/suv-car.png b/images/suv-car.png new file mode 100644 index 0000000..e7527af Binary files /dev/null and b/images/suv-car.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..955bbab --- /dev/null +++ b/index.html @@ -0,0 +1,74 @@ + + + + + + Monitoring Slot Parkir + + + + + +
+ +
+
+

Monitoring Slot Parkir

+ +
+ Logo Kendaraan +

Parkir Slot 1

+

Status: Tersedia

+
+ +
+ Logo Kendaraan +

Parkir Slot 2

+

Status: Tersedia

+
+ +
+ Logo Kendaraan +

Parkir Slot 3

+

Status: Tersedia

+
+ +
+

Denah Lokasi Parkir

+
+ Denah Lokasi Parkir +
1
+
2
+
3
+
+ +
+
+
+ Tersedia +
+
+
+ Terisi +
+
+ +
+

Notes: Tempat parkir berada di sebelah kanan, dekat dengan palang parkir.

+
+
+
+ + + + + diff --git a/script.js b/script.js new file mode 100644 index 0000000..8b375a9 --- /dev/null +++ b/script.js @@ -0,0 +1,577 @@ +// FIREBASE KONFIGURASI +import { initializeApp } from "https://www.gstatic.com/firebasejs/9.6.1/firebase-app.js"; +import { getDatabase, ref, set, onValue, push } from "https://www.gstatic.com/firebasejs/9.6.1/firebase-database.js"; + +// Firebase Configuration +const firebaseConfig = { + apiKey: "AIzaSyAS1qxaUCFRZ1Fe25qGK27JtwCWEPuaYys", + authDomain: "parkingslot-c5fba.firebaseapp.com", + databaseURL: "https://parkingslot-c5fba-default-rtdb.firebaseio.com", + projectId: "parkingslot-c5fba", + storageBucket: "parkingslot-c5fba.firebasestorage.app", + messagingSenderId: "675813222621", + appId: "1:675813222621:web:2986b33a1e5fd10c5b9334" +}; + +// Initialize Firebase +const app = initializeApp(firebaseConfig); +const db = getDatabase(app); +console.log("Firebase berhasil terhubung:", db); + +// Global Variables +let isAdminMode = false; +let previousSlotStatus = { + slot_1: null, // null berarti belum pernah di-set + slot_2: null, + slot_3: null +}; +let slotStatus = { + slot_1: "Tersedia", + slot_2: "Tersedia", + slot_3: "Tersedia" +}; +let isSystemInitialized = false; + +// Set isAdminMode true jika di halaman admin.html +if (window.location.pathname.includes('admin.html')) { + isAdminMode = true; + console.log("Mode Admin aktif"); +} else { + console.log("Mode User aktif"); +} + +// Initialize previous status dari Firebase saat startup +function initializePreviousStatus() { + console.log("πŸ”„ Menginisialisasi status sebelumnya dari Firebase..."); + + return new Promise((resolve, reject) => { + let loadedSlots = 0; + const totalSlots = 3; + + // Load status sebelumnya dari Firebase untuk setiap slot + for (let i = 1; i <= 3; i++) { + const slotKey = `slot_${i}`; + + onValue(ref(db, `parkir/slot_${i}`), function(snapshot) { + const data = snapshot.val(); + if (data && data.status) { + previousSlotStatus[slotKey] = data.status; + console.log(`βœ… Status awal slot ${i}: ${data.status}`); + } else { + // Jika tidak ada data, set default + previousSlotStatus[slotKey] = "Tersedia"; + console.log(`⚠️ Slot ${i} tidak ada data, set default: Tersedia`); + } + + loadedSlots++; + if (loadedSlots === totalSlots) { + isSystemInitialized = true; + console.log("βœ… Semua slot berhasil diinisialisasi"); + console.log("Previous Status:", previousSlotStatus); + resolve(); + } + }, { onlyOnce: true }); + } + + // Timeout fallback jika Firebase lambat + setTimeout(() => { + if (!isSystemInitialized) { + console.log("⏰ Timeout inisialisasi, menggunakan default values"); + previousSlotStatus = { + slot_1: "Tersedia", + slot_2: "Tersedia", + slot_3: "Tersedia" + }; + isSystemInitialized = true; + resolve(); + } + }, 5000); + }); +} + +// Setup MQTT +const client = mqtt.connect('ws://broker.hivemq.com:8000/mqtt'); +client.on('connect', function () { + console.log("πŸ”— Terhubung ke Broker MQTT"); + client.subscribe('parkir/slot_1'); + client.subscribe('parkir/slot_2'); + client.subscribe('parkir/slot_3'); + client.subscribe('parkir/status'); + + // Initialize previous status setelah MQTT connect + setTimeout(async () => { + await initializePreviousStatus(); + console.log("πŸš€ Sistem siap menerima data MQTT"); + }, 1000); +}); + +client.on('message', function (topic, message) { + // Jangan proses jika sistem belum diinisialisasi + if (!isSystemInitialized) { + console.log("⏳ Sistem belum siap, mengabaikan pesan MQTT"); + return; + } + + console.log(`πŸ“¨ Pesan MQTT diterima: ${message.toString()} di topik: ${topic}`); + const distance = parseInt(message.toString()); + + if (isNaN(distance)) { + console.error('❌ Data tidak valid diterima:', message.toString()); + return; + } + + // Process each slot + if (topic === 'parkir/slot_1') { + processSlotUpdate(1, distance, 'mqtt'); + } + if (topic === 'parkir/slot_2') { + processSlotUpdate(2, distance, 'mqtt'); + } + if (topic === 'parkir/slot_3') { + processSlotUpdate(3, distance, 'mqtt'); + } + + checkParkingStatus(); +}); + +// Fungsi utama untuk memproses update slot - DIPERBAIKI +function processSlotUpdate(slotNumber, distance, source = 'unknown') { + const newStatus = distance > 5 ? "Tersedia" : "Terisi"; + const slotKey = `slot_${slotNumber}`; + + console.log(`πŸ”„ Processing slot ${slotNumber}: distance=${distance}, newStatus=${newStatus}, source=${source}`); + console.log(`πŸ“Š Previous status slot ${slotNumber}: ${previousSlotStatus[slotKey]}`); + + // Update status slot di UI + updateSlotStatus(slotNumber, distance); + + // Cek perubahan status - LOGIKA DIPERBAIKI + const prevStatus = previousSlotStatus[slotKey]; + + // Jika ini adalah data pertama (previous status null), langsung log + if (prevStatus === null) { + console.log(`πŸ†• DATA PERTAMA - Slot ${slotNumber}: null -> ${newStatus}`); + logParkingHistory(slotNumber, newStatus, isAdminMode, source); + previousSlotStatus[slotKey] = newStatus; + } + // Jika ada perubahan status dari status sebelumnya + else if (prevStatus !== newStatus) { + console.log(`πŸ”₯ PERUBAHAN TERDETEKSI - Slot ${slotNumber}: ${prevStatus} -> ${newStatus}`); + console.log(`πŸ“ Mode saat ini: ${isAdminMode ? 'Admin' : 'User'}`); + + // SELALU log parking history baik di user maupun admin mode + logParkingHistory(slotNumber, newStatus, isAdminMode, source); + + // Update previous status SETELAH logging + previousSlotStatus[slotKey] = newStatus; + console.log(`βœ… Previous status slot ${slotNumber} diupdate ke: ${newStatus}`); + } else { + console.log(`➑️ Tidak ada perubahan status slot ${slotNumber}: ${newStatus}`); + } + + // Update current status + slotStatus[slotKey] = newStatus; +} + +// Fungsi untuk update indikator pada denah +function updateMapIndicator(slot, status) { + const indicator = document.getElementById('indicator_' + slot); + if (indicator) { + indicator.classList.remove('available', 'occupied'); + if (status === "Tersedia") { + indicator.classList.add('available'); + indicator.style.backgroundColor = '#4CAF50'; + } else { + indicator.classList.add('occupied'); + indicator.style.backgroundColor = '#f44336'; + } + indicator.title = `Slot ${slot} - ${status}`; + console.log(`πŸ—ΊοΈ Indikator denah slot ${slot} diupdate: ${status}`); + } +} + +function updateSlotStatus(slot, distance) { + const statusElement = document.getElementById('status_' + slot); + const slotElement = document.getElementById('sensor_' + slot); + + let status; + if (distance > 5) { + status = "Tersedia"; + if (statusElement) { + statusElement.textContent = "Status: Tersedia"; + statusElement.className = "status empty"; + } + if (slotElement) { + slotElement.style.backgroundColor = "lightgreen"; + } + } else { + status = "Terisi"; + if (statusElement) { + statusElement.textContent = "Status: Terisi"; + statusElement.className = "status occupied"; + } + if (slotElement) { + slotElement.style.backgroundColor = "lightcoral"; + } + } + + updateMapIndicator(slot, status); + // Simpan ke Firebase tanpa memanggil processSlotUpdate lagi untuk menghindari loop + updateFirebaseStatusOnly(slot, distance); +} + +function checkParkingStatus() { + const allFull = slotStatus.slot_1 === "Terisi" && slotStatus.slot_2 === "Terisi" && slotStatus.slot_3 === "Terisi"; + if (allFull) { + console.log("🚫 Parkir Penuh - Palang tetap tertutup."); + client.publish('palang/status', 'Tutup'); + } else { + console.log("βœ… Ada slot tersedia - Palang dibuka."); + client.publish('palang/status', 'Buka'); + } +} + +// Fungsi khusus untuk update Firebase tanpa trigger logging +function updateFirebaseStatusOnly(slot, distance) { + const status = distance > 5 ? "Tersedia" : "Terisi"; + console.log(`πŸ’Ύ Menyimpan ke Firebase - Slot ${slot}: distance=${distance}, status=${status}`); + + set(ref(db, 'parkir/slot_' + slot), { + distance: distance, + status: status, + lastUpdate: new Date().toISOString(), + updatedBy: isAdminMode ? 'admin' : 'user' + }) + .then(() => { + console.log(`βœ… Status slot ${slot} berhasil disimpan ke Firebase`); + }) + .catch((error) => { + console.error("❌ Gagal menyimpan status ke Firebase:", error); + }); +} + +// Fungsi log parking history - DIPERBAIKI DENGAN LOGGING DETAIL +function logParkingHistory(slot, status, isAdmin, source = 'unknown') { + const timestamp = new Date().toLocaleString('id-ID'); + const historyData = { + slot: slot, + status: status, + timestamp: timestamp, + date: new Date().toISOString(), + source: isAdmin ? 'admin' : 'user', + dataSource: source, // mqtt, firebase, manual + sessionId: generateSessionId() + }; + + console.log(`πŸ“Š MENYIMPAN RIWAYAT PARKIR:`); + console.log(` - Slot: ${slot}`); + console.log(` - Status: ${status}`); + console.log(` - Mode: ${historyData.source}`); + console.log(` - Data Source: ${source}`); + console.log(` - Timestamp: ${timestamp}`); + + push(ref(db, 'parking_history'), historyData) + .then(() => { + console.log(`βœ… SUKSES! Riwayat parkir slot ${slot} tersimpan (${historyData.source})`); + + // Hanya update tampilan jika di mode admin + if (isAdmin && typeof updateHistoryDisplay === 'function') { + console.log(`πŸ”„ Mengupdate tampilan riwayat admin...`); + updateHistoryDisplay(); + } + }) + .catch((error) => { + console.error(`❌ GAGAL! Menyimpan riwayat parkir slot ${slot}:`, error); + }); +} + +// Generate session ID untuk tracking +function generateSessionId() { + return Date.now() + '_' + Math.random().toString(36).substr(2, 9); +} + +// Fungsi untuk update tampilan riwayat (hanya di admin mode) +function updateHistoryDisplay() { + // Hanya jalankan jika di admin mode + if (!isAdminMode) { + console.log("updateHistoryDisplay diabaikan - bukan mode admin"); + return; + } + + onValue(ref(db, 'parking_history'), function(snapshot) { + const historyContainer = document.getElementById('history-container'); + if (!historyContainer) { + console.log("Element history-container tidak ditemukan"); + return; + } + + const data = snapshot.val(); + if (!data) { + historyContainer.innerHTML = '
πŸ“ Belum ada riwayat parkir
'; + return; + } + + const historyArray = Object.values(data).sort((a, b) => new Date(b.date) - new Date(a.date)); + let historyHTML = '
'; + + historyArray.forEach(record => { + const statusClass = record.status === 'Terisi' ? 'entry' : 'exit'; + const statusText = record.status === 'Terisi' ? 'Masuk' : 'Keluar'; + const sourceIcon = record.source === 'admin' ? 'πŸ”§' : 'πŸ‘€'; + const dataSourceIcon = record.dataSource === 'mqtt' ? 'πŸ“‘' : 'πŸ’Ύ'; + + historyHTML += ` +
+
+ Slot ${record.slot} - ${statusText} + ${sourceIcon} ${record.source} ${dataSourceIcon} ${record.dataSource || 'unknown'} +
+
${record.timestamp}
+
+ `; + }); + + historyHTML += '
'; + historyContainer.innerHTML = historyHTML; + console.log(`πŸ“‹ Tampilan riwayat diupdate dengan ${historyArray.length} record`); + }); +} + +// Listener realtime Firebase untuk semua slot - DIPERBAIKI +function setupRealtimeListeners() { + console.log("πŸ”₯ Setting up Firebase realtime listeners..."); + + // Listener untuk slot 1 + onValue(ref(db, 'parkir/slot_1'), function(snapshot) { + const data = snapshot.val(); + if (data) { + console.log(`πŸ”₯ Firebase realtime update slot 1: distance=${data.distance}, status=${data.status}`); + + // Hanya update UI, jangan trigger processSlotUpdate untuk menghindari double logging + const statusElement = document.getElementById('status_1'); + const slotElement = document.getElementById('sensor_1'); + + if (data.distance > 5) { + if (statusElement) { + statusElement.textContent = "Status: Tersedia"; + statusElement.className = "status empty"; + } + if (slotElement) { + slotElement.style.backgroundColor = "lightgreen"; + } + } else { + if (statusElement) { + statusElement.textContent = "Status: Terisi"; + statusElement.className = "status occupied"; + } + if (slotElement) { + slotElement.style.backgroundColor = "lightcoral"; + } + } + + updateMapIndicator(1, data.status); + slotStatus.slot_1 = data.status; + } + }); + + // Listener untuk slot 2 + onValue(ref(db, 'parkir/slot_2'), function(snapshot) { + const data = snapshot.val(); + if (data) { + console.log(`πŸ”₯ Firebase realtime update slot 2: distance=${data.distance}, status=${data.status}`); + + const statusElement = document.getElementById('status_2'); + const slotElement = document.getElementById('sensor_2'); + + if (data.distance > 5) { + if (statusElement) { + statusElement.textContent = "Status: Tersedia"; + statusElement.className = "status empty"; + } + if (slotElement) { + slotElement.style.backgroundColor = "lightgreen"; + } + } else { + if (statusElement) { + statusElement.textContent = "Status: Terisi"; + statusElement.className = "status occupied"; + } + if (slotElement) { + slotElement.style.backgroundColor = "lightcoral"; + } + } + + updateMapIndicator(2, data.status); + slotStatus.slot_2 = data.status; + } + }); + + // Listener untuk slot 3 + onValue(ref(db, 'parkir/slot_3'), function(snapshot) { + const data = snapshot.val(); + if (data) { + console.log(`πŸ”₯ Firebase realtime update slot 3: distance=${data.distance}, status=${data.status}`); + + const statusElement = document.getElementById('status_3'); + const slotElement = document.getElementById('sensor_3'); + + if (data.distance > 5) { + if (statusElement) { + statusElement.textContent = "Status: Tersedia"; + statusElement.className = "status empty"; + } + if (slotElement) { + slotElement.style.backgroundColor = "lightcoral"; + } + } else { + if (statusElement) { + statusElement.textContent = "Status: Terisi"; + statusElement.className = "status occupied"; + } + if (slotElement) { + slotElement.style.backgroundColor = "lightcoral"; + } + } + + updateMapIndicator(3, data.status); + slotStatus.slot_3 = data.status; + } + }); + + console.log("βœ… Realtime listeners untuk Firebase telah diatur"); +} + +// Mode switching functions (Admin login modal etc.) +window.showAdminLogin = function () { + const modal = document.getElementById('admin-modal'); + if (modal) modal.style.display = 'flex'; +} + +window.closeAdminModal = function () { + const modal = document.getElementById('admin-modal'); + const input = document.getElementById('admin-password'); + if (modal) modal.style.display = 'none'; + if (input) input.value = ''; +} + +window.loginAdmin = function () { + const input = document.getElementById('admin-password'); + if (input && input.value === 'admin123') { + window.location.href = 'admin.html'; + } else { + alert('Password salah!'); + } +} + +window.handlePasswordKeypress = function (event) { + if (event.key === 'Enter') { + loginAdmin(); + } +} + +// Update admin status display +function updateAdminStatus() { + if (isAdminMode) { + for (let i = 1; i <= 3; i++) { + const userStatus = document.getElementById('status_' + i)?.textContent; + const adminElement = document.getElementById('admin-status-' + i); + if (adminElement && userStatus) { + adminElement.textContent = userStatus.replace('Status: ', ''); + adminElement.style.color = userStatus.includes('Tersedia') ? '#4CAF50' : '#f44336'; + adminElement.style.fontWeight = 'bold'; + } + } + } +} + +// FUNGSI DEBUG YANG DITINGKATKAN +window.debugParkingSystem = function() { + console.log("=== πŸ” DEBUG PARKING SYSTEM ==="); + console.log("Mode:", isAdminMode ? "Admin" : "User"); + console.log("System Initialized:", isSystemInitialized); + console.log("Current Slot Status:", slotStatus); + console.log("Previous Slot Status:", previousSlotStatus); + console.log("Firebase DB Connected:", !!db); + console.log("MQTT Client Connected:", client.connected); + console.log("==============================="); +} + +// Fungsi untuk testing manual - DIPERBAIKI +window.testHistoryLogging = function(slot, status) { + console.log(`πŸ§ͺ MANUAL TEST - Logging history for slot ${slot} with status ${status}`); + + // Simulasi perubahan status + const slotKey = `slot_${slot}`; + const currentPrev = previousSlotStatus[slotKey]; + + console.log(`πŸ“Š Before test - Previous: ${currentPrev}, New: ${status}`); + + // Force log tanpa cek perubahan + logParkingHistory(slot, status, isAdminMode, 'manual-test'); + + // Update previous status + previousSlotStatus[slotKey] = status; + console.log(`βœ… Test completed - Previous status updated to: ${status}`); +} + +// FUNGSI KHUSUS UNTUK FORCE LOGGING (Testing) +window.forceLogHistory = function(slot, status) { + console.log(`🚨 FORCE LOGGING - Slot ${slot}, Status: ${status}`); + logParkingHistory(slot, status, isAdminMode, 'force-test'); +} + +// Inisialisasi saat DOM ready +window.addEventListener('DOMContentLoaded', function() { + console.log(`πŸš€ Sistem parkir dimulai - Mode: ${isAdminMode ? 'πŸ”§ Admin' : 'πŸ‘€ User'}`); + + // Inisialisasi indikator denah + for (let i = 1; i <= 3; i++) { + updateMapIndicator(i, "Tersedia"); + } + console.log("πŸ—ΊοΈ Indikator denah telah diinisialisasi"); + + // Setup realtime listeners + setupRealtimeListeners(); + + // Jika admin mode, setup tampilan riwayat dan update berkala + if (isAdminMode) { + console.log("πŸ”§ Mengaktifkan fitur admin..."); + updateHistoryDisplay(); + + // Update admin status setiap detik + setInterval(updateAdminStatus, 1000); + + console.log("βœ… Admin mode: Pemantauan riwayat realtime aktif"); + } else { + console.log("πŸ‘€ User mode: Siap mengirim data riwayat ke Firebase"); + + // Test functions untuk user mode + window.testUserMode = function() { + console.log("πŸ§ͺ Testing user mode logging..."); + console.log("πŸ” Current state:", { + initialized: isSystemInitialized, + previousStatus: previousSlotStatus, + currentStatus: slotStatus + }); + + // Test dengan force logging + forceLogHistory(1, "Terisi"); + setTimeout(() => forceLogHistory(1, "Tersedia"), 3000); + }; + + // Fungsi untuk simulasi sensor + window.simulateSensor = function(slot, distance) { + console.log(`🎯 Simulasi sensor slot ${slot} dengan jarak ${distance}cm`); + processSlotUpdate(slot, distance, 'simulation'); + }; + } + + console.log("βœ… Inisialisasi sistem parkir selesai"); + console.log("πŸ’‘ Fungsi debugging:"); + console.log(" - debugParkingSystem() : Info sistem"); + console.log(" - forceLogHistory(slot, status) : Force log history"); + if (!isAdminMode) { + console.log(" - testUserMode() : Test user mode logging"); + console.log(" - simulateSensor(slot, distance) : Simulasi sensor"); + } +}); \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000..089d9d3 --- /dev/null +++ b/style.css @@ -0,0 +1,236 @@ +body { + font-family: Arial, sans-serif; + background-color: #f4f4f9; + text-align: center; + padding: 20px; + margin: 0; +} +h1 { + color: #4CAF50; + font-size: 2.5em; + margin-bottom: 30px; +} +.mode-buttons { + margin-bottom: 30px; +} +.mode-btn { + background-color: #4CAF50; + color: white; + border: none; + padding: 12px 24px; + margin: 0 10px; + border-radius: 5px; + cursor: pointer; + font-size: 16px; + transition: background-color 0.3s; +} +.mode-btn:hover { + background-color: #45a049; +} +.sensor { + background-color: #fff; + border-radius: 8px; + margin: 20px; + padding: 20px; + display: inline-block; + width: 250px; + position: relative; + transition: background-color 0.3s ease; +} +.sensor h3 { + margin-top: 0; + font-size: 1.2em; +} +.sensor img { + position: absolute; + top: 20px; + left: 1px; + width: 80px; + height: 80px; +} +.status { + font-weight: bold; + padding: 10px; + margin-top: 10px; + border-radius: 5px; + font-size: 1.1em; +} +.status.empty { + background-color: lightgreen; + color: white; +} +.status.occupied { + background-color: lightcoral; + color: white; +} +p { + font-size: 1.1em; + margin: 0; +} +.map-container { + margin-top: 40px; + text-align: center; +} +.map-container img { + width: 80%; + max-width: 600px; + height: auto; +} +.direction { + margin-top: 10px; + font-size: 1.2em; + color: #333; +} +/* Modal styles */ +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.5); + justify-content: center; + align-items: center; +} +.modal-content { + background-color: white; + padding: 30px; + border-radius: 10px; + text-align: center; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); + min-width: 300px; +} +.modal h2 { + margin-top: 0; + color: #4CAF50; +} +.modal input { + width: 200px; + padding: 10px; + margin: 10px 0; + border: 1px solid #ddd; + border-radius: 5px; + font-size: 16px; +} +.modal button { + background-color: #4CAF50; + color: white; + border: none; + padding: 10px 20px; + margin: 5px; + border-radius: 5px; + cursor: pointer; + font-size: 16px; +} +.modal button:hover { + background-color: #45a049; +} +.modal .cancel-btn { + background-color: #f44336; +} +.modal .cancel-btn:hover { + background-color: #da190b; +} + +/* Perbaikan tampilan parking-history di admin */ + +.back-btn { + background-color: #2196F3; + color: white; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + font-size: 16px; + margin-bottom: 20px; +} +.back-btn:hover { + background-color: #0b7dda; +} + +/* Styling untuk indikator slot pada denah */ +.map-indicators { + position: relative; + display: inline-block; + margin-top: 10px; +} + +.slot-indicator { + position: absolute; + width: 15px; + height: 15px; + border-radius: 3px; + background-color: #4CAF50; + border: 2px solid #fff; + box-shadow: 0 2px 4px rgba(0,0,0,0.3); + display: flex; + align-items: center; + justify-content: center; + font-size: 8px; + font-weight: bold; + color: white; + text-shadow: 1px 1px 1px rgba(0,0,0,0.5); + transition: all 0.3s ease; +} + +.slot-indicator.occupied { + background-color: #f44336; +} + +.slot-indicator.available { + background-color: #4CAF50; +} + +/* Posisi indikator pada denah - sesuaikan dengan lokasi parkir Anda */ +#indicator_1 { + top: 39%; + right: 27%; +} + +#indicator_2 { + top: 51%; + right: 27%; +} + +#indicator_3 { + top: 63%; + right: 27%; +} + +/* Efek hover untuk indikator */ +.slot-indicator:hover { + transform: scale(1.2); + z-index: 10; +} + +/* Legend untuk indikator */ +.map-legend { + margin-top: 10px; + display: flex; + justify-content: center; + gap: 20px; + font-size: 12px; +} + +.legend-item { + display: flex; + align-items: center; + gap: 5px; +} + +.legend-color { + width: 12px; + height: 12px; + border-radius: 2px; + border: 1px solid #ccc; +} + +.legend-available { + background-color: #4CAF50; +} + +.legend-occupied { + background-color: #f44336; +} \ No newline at end of file