maps updates
This commit is contained in:
94
map/app/public/admin.html
Normal file
94
map/app/public/admin.html
Normal file
@@ -0,0 +1,94 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Admin Panel - NocoDB Map Viewer">
|
||||
<title>Admin Panel - NocoDB Map Viewer</title>
|
||||
|
||||
<!-- Leaflet CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin="" />
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="stylesheet" href="css/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<h1>Admin Panel</h1>
|
||||
<div class="header-actions">
|
||||
<a href="/" class="btn btn-secondary">← Back to Map</a>
|
||||
<span id="admin-info" class="admin-info"></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="admin-container">
|
||||
<div class="admin-sidebar">
|
||||
<h2>Settings</h2>
|
||||
<nav class="admin-nav">
|
||||
<a href="#start-location" class="active">Start Location</a>
|
||||
<!-- Future menu items can go here -->
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="admin-content">
|
||||
<!-- Start Location Section -->
|
||||
<section id="start-location" class="admin-section">
|
||||
<h2>Map Start Location</h2>
|
||||
<p>Set the default center point and zoom level for the map when users first load the application.</p>
|
||||
|
||||
<div class="admin-map-container">
|
||||
<div id="admin-map" class="admin-map"></div>
|
||||
|
||||
<div class="location-controls">
|
||||
<div class="form-group">
|
||||
<label for="start-lat">Latitude</label>
|
||||
<input type="number" id="start-lat" step="0.000001" min="-90" max="90">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="start-lng">Longitude</label>
|
||||
<input type="number" id="start-lng" step="0.000001" min="-180" max="180">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="start-zoom">Zoom Level</label>
|
||||
<input type="number" id="start-zoom" min="2" max="19" step="1">
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button id="use-current-view" class="btn btn-secondary">
|
||||
Use Current Map View
|
||||
</button>
|
||||
<button id="save-start-location" class="btn btn-primary">
|
||||
Save Start Location
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="help-text">
|
||||
<p>💡 Tip: Navigate the map to your desired location and zoom level, then click "Use Current Map View" to capture the coordinates.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Messages -->
|
||||
<div id="status-container" class="status-container"></div>
|
||||
</div>
|
||||
|
||||
<!-- Leaflet JS -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
crossorigin=""></script>
|
||||
|
||||
<!-- Admin JavaScript -->
|
||||
<script src="js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
268
map/app/public/css/admin.css
Normal file
268
map/app/public/css/admin.css
Normal file
@@ -0,0 +1,268 @@
|
||||
/* Admin Panel Specific Styles */
|
||||
.admin-container {
|
||||
display: flex;
|
||||
height: calc(100vh - var(--header-height));
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.admin-sidebar {
|
||||
width: 250px;
|
||||
background-color: white;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.admin-sidebar h2 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 20px;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
.admin-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.admin-nav a {
|
||||
padding: 10px 15px;
|
||||
color: var(--dark-color);
|
||||
text-decoration: none;
|
||||
border-radius: var(--border-radius);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.admin-nav a:hover {
|
||||
background-color: var(--light-color);
|
||||
}
|
||||
|
||||
.admin-nav a.active {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.admin-content {
|
||||
flex: 1;
|
||||
padding: 30px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.admin-section {
|
||||
background-color: white;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 30px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.admin-section h2 {
|
||||
margin-bottom: 15px;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
.admin-section p {
|
||||
color: #666;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.admin-map-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 300px;
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.admin-map {
|
||||
height: 500px;
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.location-controls {
|
||||
padding: 20px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.location-controls .form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.location-controls .form-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background-color: #e3f2fd;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.help-text p {
|
||||
margin: 0;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.admin-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: rgba(255,255,255,0.9);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Form styles */
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
|
||||
}
|
||||
|
||||
/* Button styles */
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #45a049;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #5a6268;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Status messages */
|
||||
.status-container {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 10000;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.status-message {
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 14px;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.status-message.success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.status-message.error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.status-message.info {
|
||||
background-color: #cce7ff;
|
||||
color: #004085;
|
||||
border: 1px solid #b3d7ff;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.admin-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.admin-sidebar {
|
||||
width: 100%;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.admin-map-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.admin-map {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.admin-content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.admin-section {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* CSS Variables (define these in style.css if not already defined) */
|
||||
:root {
|
||||
--primary-color: #4CAF50;
|
||||
--dark-color: #333;
|
||||
--light-color: #f5f5f5;
|
||||
--border-radius: 6px;
|
||||
--transition: all 0.2s ease;
|
||||
--header-height: 60px;
|
||||
}
|
||||
@@ -549,6 +549,79 @@ body {
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
/* Distinctive start location marker styles */
|
||||
.start-location-custom-marker {
|
||||
z-index: 2000 !important;
|
||||
}
|
||||
|
||||
.start-location-marker-wrapper {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.start-location-marker-pin {
|
||||
position: absolute;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: #ff4444;
|
||||
border-radius: 50% 50% 50% 0;
|
||||
transform: rotate(-45deg);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 3px solid white;
|
||||
animation: bounce-marker 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.start-location-marker-inner {
|
||||
transform: rotate(45deg);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.start-location-marker-pulse {
|
||||
position: absolute;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 68, 68, 0.3);
|
||||
animation: pulse-ring 2s ease-out infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce-marker {
|
||||
0%, 100% {
|
||||
transform: rotate(-45deg) translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(-45deg) translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-ring {
|
||||
0% {
|
||||
transform: scale(0.5);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced popup for start location */
|
||||
.start-location-popup-enhanced .leaflet-popup-content-wrapper {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
box-shadow: 0 5px 20px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.start-location-popup-enhanced .leaflet-popup-content {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.header h1 {
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
<button id="geolocate-btn" class="btn btn-primary" title="Find my location">
|
||||
<span class="btn-icon">📍</span><span class="btn-text">My Location</span>
|
||||
</button>
|
||||
<button id="toggle-start-location-btn" class="btn btn-secondary" title="Toggle start location marker">
|
||||
<span class="btn-icon">📍</span><span class="btn-text">Hide Start Location</span>
|
||||
</button>
|
||||
<button id="add-location-btn" class="btn btn-success" title="Add location at map center">
|
||||
<span class="btn-icon">➕</span><span class="btn-text">Add Location Here</span>
|
||||
</button>
|
||||
|
||||
259
map/app/public/js/admin.js
Normal file
259
map/app/public/js/admin.js
Normal file
@@ -0,0 +1,259 @@
|
||||
// Admin panel JavaScript
|
||||
let adminMap = null;
|
||||
let startMarker = null;
|
||||
|
||||
// Initialize when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
checkAdminAuth();
|
||||
initializeAdminMap();
|
||||
loadCurrentStartLocation();
|
||||
setupEventListeners();
|
||||
});
|
||||
|
||||
// Check if user is authenticated as admin
|
||||
async function checkAdminAuth() {
|
||||
try {
|
||||
const response = await fetch('/api/auth/check');
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.authenticated || !data.user?.isAdmin) {
|
||||
window.location.href = '/login.html';
|
||||
return;
|
||||
}
|
||||
|
||||
// Display admin info
|
||||
document.getElementById('admin-info').innerHTML = `
|
||||
<span>👤 ${escapeHtml(data.user.email)}</span>
|
||||
<button id="logout-btn" class="btn btn-secondary btn-sm">Logout</button>
|
||||
`;
|
||||
|
||||
document.getElementById('logout-btn').addEventListener('click', handleLogout);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Auth check failed:', error);
|
||||
window.location.href = '/login.html';
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the admin map
|
||||
function initializeAdminMap() {
|
||||
adminMap = L.map('admin-map').setView([53.5461, -113.4938], 11);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
maxZoom: 19,
|
||||
minZoom: 2
|
||||
}).addTo(adminMap);
|
||||
|
||||
// Add click handler to set location
|
||||
adminMap.on('click', handleMapClick);
|
||||
|
||||
// Update coordinates when map moves
|
||||
adminMap.on('moveend', updateCoordinatesFromMap);
|
||||
}
|
||||
|
||||
// Load current start location
|
||||
async function loadCurrentStartLocation() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/start-location');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
const { latitude, longitude, zoom } = data.location;
|
||||
|
||||
// Update form fields
|
||||
document.getElementById('start-lat').value = latitude;
|
||||
document.getElementById('start-lng').value = longitude;
|
||||
document.getElementById('start-zoom').value = zoom;
|
||||
|
||||
// Update map
|
||||
adminMap.setView([latitude, longitude], zoom);
|
||||
updateStartMarker(latitude, longitude);
|
||||
|
||||
// Show source info
|
||||
if (data.source) {
|
||||
const sourceText = data.source === 'database' ? 'Loaded from database' :
|
||||
data.source === 'environment' ? 'Using environment defaults' :
|
||||
'Using system defaults';
|
||||
showStatus(sourceText, 'info');
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load start location:', error);
|
||||
showStatus('Failed to load current start location', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle map click
|
||||
function handleMapClick(e) {
|
||||
const { lat, lng } = e.latlng;
|
||||
|
||||
document.getElementById('start-lat').value = lat.toFixed(6);
|
||||
document.getElementById('start-lng').value = lng.toFixed(6);
|
||||
|
||||
updateStartMarker(lat, lng);
|
||||
}
|
||||
|
||||
// Update marker position
|
||||
function updateStartMarker(lat, lng) {
|
||||
if (startMarker) {
|
||||
startMarker.setLatLng([lat, lng]);
|
||||
} else {
|
||||
startMarker = L.marker([lat, lng], {
|
||||
draggable: true,
|
||||
title: 'Start Location'
|
||||
}).addTo(adminMap);
|
||||
|
||||
// Update coordinates when marker is dragged
|
||||
startMarker.on('dragend', (e) => {
|
||||
const position = e.target.getLatLng();
|
||||
document.getElementById('start-lat').value = position.lat.toFixed(6);
|
||||
document.getElementById('start-lng').value = position.lng.toFixed(6);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update coordinates from current map view
|
||||
function updateCoordinatesFromMap() {
|
||||
const center = adminMap.getCenter();
|
||||
const zoom = adminMap.getZoom();
|
||||
|
||||
document.getElementById('start-zoom').value = zoom;
|
||||
}
|
||||
|
||||
// Setup event listeners
|
||||
function setupEventListeners() {
|
||||
// Use current view button
|
||||
document.getElementById('use-current-view').addEventListener('click', () => {
|
||||
const center = adminMap.getCenter();
|
||||
const zoom = adminMap.getZoom();
|
||||
|
||||
document.getElementById('start-lat').value = center.lat.toFixed(6);
|
||||
document.getElementById('start-lng').value = center.lng.toFixed(6);
|
||||
document.getElementById('start-zoom').value = zoom;
|
||||
|
||||
updateStartMarker(center.lat, center.lng);
|
||||
showStatus('Captured current map view', 'success');
|
||||
});
|
||||
|
||||
// Save button
|
||||
document.getElementById('save-start-location').addEventListener('click', saveStartLocation);
|
||||
|
||||
// Coordinate input changes
|
||||
document.getElementById('start-lat').addEventListener('change', updateMapFromInputs);
|
||||
document.getElementById('start-lng').addEventListener('change', updateMapFromInputs);
|
||||
document.getElementById('start-zoom').addEventListener('change', updateMapFromInputs);
|
||||
}
|
||||
|
||||
// Update map from input fields
|
||||
function updateMapFromInputs() {
|
||||
const lat = parseFloat(document.getElementById('start-lat').value);
|
||||
const lng = parseFloat(document.getElementById('start-lng').value);
|
||||
const zoom = parseInt(document.getElementById('start-zoom').value);
|
||||
|
||||
if (!isNaN(lat) && !isNaN(lng) && !isNaN(zoom)) {
|
||||
adminMap.setView([lat, lng], zoom);
|
||||
updateStartMarker(lat, lng);
|
||||
}
|
||||
}
|
||||
|
||||
// Save start location
|
||||
async function saveStartLocation() {
|
||||
const lat = parseFloat(document.getElementById('start-lat').value);
|
||||
const lng = parseFloat(document.getElementById('start-lng').value);
|
||||
const zoom = parseInt(document.getElementById('start-zoom').value);
|
||||
|
||||
// Validate
|
||||
if (isNaN(lat) || isNaN(lng) || isNaN(zoom)) {
|
||||
showStatus('Please enter valid coordinates and zoom level', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (lat < -90 || lat > 90 || lng < -180 || lng > 180) {
|
||||
showStatus('Coordinates out of valid range', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (zoom < 2 || zoom > 19) {
|
||||
showStatus('Zoom level must be between 2 and 19', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/start-location', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
latitude: lat,
|
||||
longitude: lng,
|
||||
zoom: zoom
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showStatus('Start location saved successfully!', 'success');
|
||||
} else {
|
||||
throw new Error(data.error || 'Failed to save');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Save error:', error);
|
||||
showStatus(error.message || 'Failed to save start location', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle logout
|
||||
async function handleLogout() {
|
||||
if (!confirm('Are you sure you want to logout?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
window.location.href = '/login.html';
|
||||
} else {
|
||||
showStatus('Logout failed. Please try again.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
showStatus('Logout failed. Please try again.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Show status message
|
||||
function showStatus(message, type = 'info') {
|
||||
const container = document.getElementById('status-container');
|
||||
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `status-message ${type}`;
|
||||
messageDiv.textContent = message;
|
||||
|
||||
container.appendChild(messageDiv);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
messageDiv.remove();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Escape HTML
|
||||
function escapeHtml(text) {
|
||||
if (text === null || text === undefined) {
|
||||
return '';
|
||||
}
|
||||
const div = document.createElement('div');
|
||||
div.textContent = String(text);
|
||||
return div.innerHTML;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user