@ -616,6 +616,81 @@
display: block;
display: block;
animation: slideUp 0.3s ease;
animation: slideUp 0.3s ease;
}
}
/* Geocache List Sidebar */
.geocache-list-sidebar {
position: fixed;
right: -350px;
top: 60px;
bottom: 0;
width: 350px;
background: rgba(30, 30, 30, 0.95);
backdrop-filter: blur(10px);
transition: right 0.3s ease;
z-index: 999;
overflow-y: auto;
padding: 20px;
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.5);
}
.geocache-list-sidebar.open {
right: 0;
}
.geocache-list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #444;
}
.geocache-list-item {
background: rgba(40, 40, 40, 0.8);
border-radius: 8px;
padding: 12px;
margin-bottom: 10px;
cursor: pointer;
transition: background 0.2s;
}
.geocache-list-item:hover {
background: rgba(60, 60, 60, 0.9);
}
.geocache-list-item-title {
font-weight: bold;
color: #FFA726;
margin-bottom: 5px;
display: flex;
align-items: center;
gap: 8px;
}
.geocache-list-item-info {
font-size: 0.9em;
color: #aaa;
}
.geocache-list-item-secret {
display: inline-block;
background: #9C27B0;
color: white;
padding: 2px 6px;
border-radius: 4px;
font-size: 0.75em;
margin-left: 5px;
}
.geocache-list-toggle {
position: fixed;
right: 20px;
bottom: 140px;
width: 50px;
height: 50px;
background: #FFA726;
border-radius: 50%;
display: none;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
z-index: 998;
}
/* Admin Panel Styles */
/* Admin Panel Styles */
.admin-setting-group {
.admin-setting-group {
padding: 15px 10px;
padding: 15px 10px;
@ -974,6 +1049,19 @@
< input type = "text" id = "geocacheIconInput" placeholder = "package-variant" value = "package-variant" maxlength = "50" >
< input type = "text" id = "geocacheIconInput" placeholder = "package-variant" value = "package-variant" maxlength = "50" >
< small style = "color: #888; display: block; margin-top: 4px;" > Browse icons at < a href = "https://pictogrammers.com/library/mdi/" target = "_blank" style = "color: #FFA726;" > Material Design Icons< / a > < / small >
< small style = "color: #888; display: block; margin-top: 4px;" > Browse icons at < a href = "https://pictogrammers.com/library/mdi/" target = "_blank" style = "color: #FFA726;" > Material Design Icons< / a > < / small >
< / div >
< / div >
< div class = "geocache-input-group" id = "geocacheColorGroup" style = "display: none;" >
< label for = "geocacheColorInput" > Icon Color< / label >
< div style = "display: flex; align-items: center; gap: 10px;" >
< input type = "color" id = "geocacheColorInput" value = "#FFA726" style = "width: 60px; height: 35px; border: 1px solid #555; border-radius: 4px; cursor: pointer;" >
< span id = "geocacheColorPreview" style = "font-size: 28px;" > < i class = "mdi mdi-package-variant" style = "color: #FFA726;" > < / i > < / span >
< button type = "button" id = "geocacheColorReset" style = "padding: 5px 10px; background: #555; color: white; border: none; border-radius: 4px; cursor: pointer;" > Reset< / button >
< / div >
< / div >
< div class = "geocache-input-group" id = "geocacheVisibilityGroup" style = "display: none;" >
< label for = "geocacheVisibilityInput" > Visibility Distance (meters, 0 = always visible)< / label >
< input type = "number" id = "geocacheVisibilityInput" placeholder = "0" value = "0" min = "0" max = "10000" step = "10" >
< small style = "color: #888; display: block; margin-top: 4px;" > Secret caches are only visible when users are within this distance< / small >
< / div >
< div class = "geocache-input-group" >
< div class = "geocache-input-group" >
< label for = "geocacheName" > Your Name< / label >
< label for = "geocacheName" > Your Name< / label >
< input type = "text" id = "geocacheName" placeholder = "Enter your name" maxlength = "50" >
< input type = "text" id = "geocacheName" placeholder = "Enter your name" maxlength = "50" >
@ -986,6 +1074,7 @@
< div class = "geocache-dialog-buttons" >
< div class = "geocache-dialog-buttons" >
< button class = "geocache-cancel-btn" id = "geocacheCancel" > Close< / button >
< button class = "geocache-cancel-btn" id = "geocacheCancel" > Close< / button >
< button class = "geocache-submit-btn" id = "geocacheSubmit" > Add Message< / button >
< button class = "geocache-submit-btn" id = "geocacheSubmit" > Add Message< / button >
< button class = "geocache-edit-btn" id = "geocacheEdit" style = "display: none; background: #4CAF50;" > Edit Cache< / button >
< button class = "geocache-delete-btn" id = "geocacheDelete" style = "display: none;" > Delete Geocache< / button >
< button class = "geocache-delete-btn" id = "geocacheDelete" style = "display: none;" > Delete Geocache< / button >
< / div >
< / div >
< / div >
< / div >
@ -996,6 +1085,22 @@
📍 Geocache nearby! Click to view messages.
📍 Geocache nearby! Click to view messages.
< / div >
< / div >
<!-- Geocache List Sidebar -->
< div id = "geocacheListSidebar" class = "geocache-list-sidebar" >
< div class = "geocache-list-header" >
< h3 style = "color: #FFA726; margin: 0;" > 📍 Geocaches< / h3 >
< button id = "geocacheListClose" style = "background: none; border: none; color: #aaa; font-size: 24px; cursor: pointer;" > ×< / button >
< / div >
< div id = "geocacheListContent" >
<!-- Will be populated dynamically -->
< / div >
< / div >
<!-- Geocache List Toggle Button -->
< div id = "geocacheListToggle" class = "geocache-list-toggle" >
< i class = "mdi mdi-map-marker-multiple" style = "font-size: 24px; color: white;" > < / i >
< / div >
<!-- Navigation confirmation dialog -->
<!-- Navigation confirmation dialog -->
< div id = "navConfirmDialog" class = "nav-confirm-dialog" style = "display: none;" >
< div id = "navConfirmDialog" class = "nav-confirm-dialog" style = "display: none;" >
< div class = "nav-confirm-content" >
< div class = "nav-confirm-content" >
@ -1142,6 +1247,7 @@
// Geocache variables
// Geocache variables
let geocaches = []; // Array of { id, lat, lng, messages: [{author, text, timestamp}] }
let geocaches = []; // Array of { id, lat, lng, messages: [{author, text, timestamp}] }
let currentGeocache = null; // Currently selected/nearby geocache
let currentGeocache = null; // Currently selected/nearby geocache
let currentGeocacheEditMode = false; // Whether we're editing an existing geocache
let geocacheMarkers = {}; // Map of geocache id to marker
let geocacheMarkers = {}; // Map of geocache id to marker
let lastGeocacheProximityCheck = 0;
let lastGeocacheProximityCheck = 0;
let readGeocaches = JSON.parse(localStorage.getItem('readGeocaches') || '[]'); // Track which caches user has read
let readGeocaches = JSON.parse(localStorage.getItem('readGeocaches') || '[]'); // Track which caches user has read
@ -1384,6 +1490,11 @@
// Store user location for geocache proximity checks
// Store user location for geocache proximity checks
userLocation = { lat, lng, accuracy };
userLocation = { lat, lng, accuracy };
// Update geocache visibility based on new location
if (navMode) {
updateGeocacheVisibility();
}
// Update or create marker
// Update or create marker
if (!gpsMarker) {
if (!gpsMarker) {
const myIcon = L.divIcon({
const myIcon = L.divIcon({
@ -2196,6 +2307,22 @@
return 'gc_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
return 'gc_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}
}
function shouldShowGeocache(geocache) {
// In edit mode, always show all geocaches
if (!navMode) return true;
// If no visibility restriction, always show
if (!geocache.visibilityDistance || geocache.visibilityDistance === 0) return true;
// In nav mode, only show if user is within visibility distance
if (!userLocation) return false;
const distance = L.latLng(userLocation.lat, userLocation.lng)
.distanceTo(L.latLng(geocache.lat, geocache.lng));
return distance < = geocache.visibilityDistance;
}
function placeGeocache(latlng) {
function placeGeocache(latlng) {
const id = generateGeocacheId();
const id = generateGeocacheId();
const geocache = {
const geocache = {
@ -2204,6 +2331,8 @@
lng: latlng.lng,
lng: latlng.lng,
title: '', // Will be set when user submits
title: '', // Will be set when user submits
icon: 'package-variant', // Default icon
icon: 'package-variant', // Default icon
color: '#FFA726', // Default orange color
visibilityDistance: 0, // 0 = always visible, >0 = only visible within that distance
messages: [],
messages: [],
createdAt: Date.now()
createdAt: Date.now()
};
};
@ -2214,14 +2343,24 @@
}
}
function createGeocacheMarker(geocache) {
function createGeocacheMarker(geocache) {
// Check visibility based on mode and distance
if (!shouldShowGeocache(geocache)) {
console.log(`Geocache ${geocache.id} not visible due to distance restriction`);
return;
}
console.log(`Creating geocache marker for ${geocache.id} at ${geocache.lat}, ${geocache.lng}`);
console.log(`Creating geocache marker for ${geocache.id} at ${geocache.lat}, ${geocache.lng}`);
// Use geocache's custom icon or default
// Use geocache's custom icon and col or
const iconClass = `mdi-${geocache.icon || 'package-variant'}`;
const iconClass = `mdi-${geocache.icon || 'package-variant'}`;
const color = geocache.color || '#FFA726';
// In edit mode, make secret caches slightly transparent
const opacity = (!navMode & & geocache.visibilityDistance > 0) ? 0.7 : 1.0;
const marker = L.marker([geocache.lat, geocache.lng], {
const marker = L.marker([geocache.lat, geocache.lng], {
icon: L.divIcon({
icon: L.divIcon({
className: 'geocache-marker',
className: 'geocache-marker',
html: `< i class = "mdi ${iconClass}" style = "font-size: 28px; color: #FFA726 ;" > < / i > `,
html: `< i class = "mdi ${iconClass}" style = "font-size: 28px; color: ${color}; opacity: ${opacity} ;" > < / i > `,
iconSize: [28, 28],
iconSize: [28, 28],
iconAnchor: [14, 28]
iconAnchor: [14, 28]
}),
}),
@ -2287,9 +2426,14 @@
const form = document.getElementById('geocacheForm');
const form = document.getElementById('geocacheForm');
const titleGroup = document.getElementById('geocacheTitleGroup');
const titleGroup = document.getElementById('geocacheTitleGroup');
const iconGroup = document.getElementById('geocacheIconGroup');
const iconGroup = document.getElementById('geocacheIconGroup');
const colorGroup = document.getElementById('geocacheColorGroup');
const visibilityGroup = document.getElementById('geocacheVisibilityGroup');
const titleInput = document.getElementById('geocacheTitleInput');
const titleInput = document.getElementById('geocacheTitleInput');
const iconInput = document.getElementById('geocacheIconInput');
const iconInput = document.getElementById('geocacheIconInput');
const colorInput = document.getElementById('geocacheColorInput');
const visibilityInput = document.getElementById('geocacheVisibilityInput');
const dialogTitle = document.getElementById('geocacheTitle');
const dialogTitle = document.getElementById('geocacheTitle');
const editBtn = document.getElementById('geocacheEdit');
// Clear previous messages
// Clear previous messages
messagesDiv.innerHTML = '';
messagesDiv.innerHTML = '';
@ -2297,17 +2441,31 @@
// Update dialog title
// Update dialog title
dialogTitle.textContent = geocache.title ? `📍 ${geocache.title}` : '📍 Geocache';
dialogTitle.textContent = geocache.title ? `📍 ${geocache.title}` : '📍 Geocache';
// Show/hide title and icon inputs for new geocaches
if (isNew & & !geocache.title) {
// Show/hide creation fields for new geocaches or edit mode
const isEditing = currentGeocacheEditMode === true;
if ((isNew & & !geocache.title) || isEditing) {
titleGroup.style.display = 'block';
titleGroup.style.display = 'block';
iconGroup.style.display = 'block';
iconGroup.style.display = 'block';
titleInput.value = '';
colorGroup.style.display = 'block';
visibilityGroup.style.display = 'block';
titleInput.value = isEditing ? geocache.title : '';
iconInput.value = geocache.icon || 'package-variant';
iconInput.value = geocache.icon || 'package-variant';
colorInput.value = geocache.color || '#FFA726';
visibilityInput.value = geocache.visibilityDistance || 0;
// Update color preview
updateColorPreview();
} else {
} else {
titleGroup.style.display = 'none';
titleGroup.style.display = 'none';
iconGroup.style.display = 'none';
iconGroup.style.display = 'none';
colorGroup.style.display = 'none';
visibilityGroup.style.display = 'none';
}
}
// Show edit button only in edit mode and for existing caches
editBtn.style.display = (!navMode & & geocache.title & & !isEditing) ? 'block' : 'none';
// Check if user can view messages (within 5m in nav mode, or in edit mode)
// Check if user can view messages (within 5m in nav mode, or in edit mode)
const canViewMessages = !navMode || userDistance < = adminSettings.geocacheRange;
const canViewMessages = !navMode || userDistance < = adminSettings.geocacheRange;
@ -2381,6 +2539,85 @@
function hideGeocacheDialog() {
function hideGeocacheDialog() {
document.getElementById('geocacheDialog').style.display = 'none';
document.getElementById('geocacheDialog').style.display = 'none';
currentGeocache = null;
currentGeocache = null;
currentGeocacheEditMode = false;
}
function updateColorPreview() {
const iconInput = document.getElementById('geocacheIconInput');
const colorInput = document.getElementById('geocacheColorInput');
const preview = document.getElementById('geocacheColorPreview');
const icon = iconInput.value || 'package-variant';
const color = colorInput.value || '#FFA726';
preview.innerHTML = `< i class = "mdi mdi-${icon}" style = "color: ${color};" > < / i > `;
}
function startEditingGeocache() {
if (!currentGeocache || !currentGeocache.title) return;
currentGeocacheEditMode = true;
showGeocacheDialog(currentGeocache, false);
}
function updateGeocacheList() {
const content = document.getElementById('geocacheListContent');
content.innerHTML = '';
if (geocaches.length === 0) {
content.innerHTML = '< p style = "color: #aaa; text-align: center;" > No geocaches placed yet< / p > ';
return;
}
geocaches.forEach(cache => {
const div = document.createElement('div');
div.className = 'geocache-list-item';
const titleDiv = document.createElement('div');
titleDiv.className = 'geocache-list-item-title';
titleDiv.innerHTML = `
< i class = "mdi mdi-${cache.icon || 'package-variant'}" style = "color: ${cache.color || '#FFA726'};" > < / i >
< span > ${cache.title || 'Untitled Cache'}< / span >
${cache.visibilityDistance > 0 ? '< span class = "geocache-list-item-secret" > SECRET< / span > ' : ''}
`;
const infoDiv = document.createElement('div');
infoDiv.className = 'geocache-list-item-info';
const messageCount = cache.messages ? cache.messages.length : 0;
const createdDate = new Date(cache.createdAt).toLocaleDateString();
infoDiv.innerHTML = `
${messageCount} message${messageCount !== 1 ? 's' : ''} • Created ${createdDate}
${cache.visibilityDistance > 0 ? `< br > Visible within ${cache.visibilityDistance}m` : ''}
`;
div.appendChild(titleDiv);
div.appendChild(infoDiv);
// Click to go to cache
div.addEventListener('click', () => {
map.setView([cache.lat, cache.lng], 16);
showGeocacheDialog(cache, false);
document.getElementById('geocacheListSidebar').classList.remove('open');
});
content.appendChild(div);
});
}
function updateGeocacheVisibility() {
// Update visibility of all geocache markers based on current user location
geocaches.forEach(cache => {
const shouldShow = shouldShowGeocache(cache);
const marker = geocacheMarkers[cache.id];
if (shouldShow & & !marker) {
// Create marker if it should be visible but doesn't exist
createGeocacheMarker(cache);
} else if (!shouldShow & & marker) {
// Remove marker if it shouldn't be visible
map.removeLayer(marker);
delete geocacheMarkers[cache.id];
}
});
}
}
function updateGeocacheMarkerIcon(geocacheId, isRead) {
function updateGeocacheMarkerIcon(geocacheId, isRead) {
@ -2396,11 +2633,15 @@
const messageInput = document.getElementById('geocacheMessage');
const messageInput = document.getElementById('geocacheMessage');
const titleInput = document.getElementById('geocacheTitleInput');
const titleInput = document.getElementById('geocacheTitleInput');
const iconInput = document.getElementById('geocacheIconInput');
const iconInput = document.getElementById('geocacheIconInput');
const colorInput = document.getElementById('geocacheColorInput');
const visibilityInput = document.getElementById('geocacheVisibilityInput');
// For new geocaches without a title, set the title and icon first
if (!currentGeocache.title) {
// For new geocaches or when editing, update all properties
if (!currentGeocache.title || currentGeocacheEditMode ) {
const title = titleInput.value.trim();
const title = titleInput.value.trim();
const icon = iconInput.value.trim();
const icon = iconInput.value.trim();
const color = colorInput.value || '#FFA726';
const visibilityDistance = parseInt(visibilityInput.value) || 0;
if (!title) {
if (!title) {
alert('Please enter a title for this geocache');
alert('Please enter a title for this geocache');
@ -2409,16 +2650,31 @@
currentGeocache.title = title;
currentGeocache.title = title;
currentGeocache.icon = icon || 'package-variant';
currentGeocache.icon = icon || 'package-variant';
currentGeocache.color = color;
currentGeocache.visibilityDistance = visibilityDistance;
// Update the marker with new icon
// Update or recreate the marker with new properties
if (geocacheMarkers[currentGeocache.id]) {
if (geocacheMarkers[currentGeocache.id]) {
const marker = geocacheMarkers[currentGeocache.id];
marker.setIcon(L.divIcon({
className: 'geocache-marker',
html: `< i class = "mdi mdi-${currentGeocache.icon}" style = "font-size: 28px; color: #FFA726;" > < / i > `,
iconSize: [28, 28],
iconAnchor: [14, 28]
}));
map.removeLayer(geocacheMarkers[currentGeocache.id]);
delete geocacheMarkers[currentGeocache.id];
}
createGeocacheMarker(currentGeocache);
// If we were editing, exit edit mode
if (currentGeocacheEditMode) {
currentGeocacheEditMode = false;
// Don't add a message when just editing properties
if (!messageInput.value.trim()) {
showGeocacheDialog(currentGeocache, false);
// Broadcast the update
if (ws & & ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'geocacheUpdate',
geocache: currentGeocache
}));
}
return;
}
}
}
}
}
@ -2529,6 +2785,24 @@
document.getElementById('geocacheCancel').addEventListener('click', hideGeocacheDialog);
document.getElementById('geocacheCancel').addEventListener('click', hideGeocacheDialog);
document.getElementById('geocacheSubmit').addEventListener('click', addGeocacheMessage);
document.getElementById('geocacheSubmit').addEventListener('click', addGeocacheMessage);
document.getElementById('geocacheDelete').addEventListener('click', deleteGeocache);
document.getElementById('geocacheDelete').addEventListener('click', deleteGeocache);
document.getElementById('geocacheEdit').addEventListener('click', startEditingGeocache);
// Color picker events
document.getElementById('geocacheIconInput').addEventListener('input', updateColorPreview);
document.getElementById('geocacheColorInput').addEventListener('input', updateColorPreview);
document.getElementById('geocacheColorReset').addEventListener('click', () => {
document.getElementById('geocacheColorInput').value = '#FFA726';
updateColorPreview();
});
// Geocache list sidebar events
document.getElementById('geocacheListToggle').addEventListener('click', () => {
document.getElementById('geocacheListSidebar').classList.toggle('open');
updateGeocacheList();
});
document.getElementById('geocacheListClose').addEventListener('click', () => {
document.getElementById('geocacheListSidebar').classList.remove('open');
});
document.getElementById('geocacheAlert').addEventListener('click', function() {
document.getElementById('geocacheAlert').addEventListener('click', function() {
// Find nearest geocache and open it
// Find nearest geocache and open it
if (userLocation) {
if (userLocation) {
@ -3248,6 +3522,10 @@
editContent.classList.add('active');
editContent.classList.add('active');
navMode = false;
navMode = false;
// Show geocache list toggle in edit mode
document.getElementById('geocacheListToggle').style.display = 'flex';
updateGeocacheVisibility();
// In edit mode, disable auto-center
// In edit mode, disable auto-center
if (autoCenterMode) {
if (autoCenterMode) {
autoCenterMode = false;
autoCenterMode = false;
@ -3272,6 +3550,11 @@
navContent.classList.add('active');
navContent.classList.add('active');
navMode = true;
navMode = true;
// Hide geocache list toggle in nav mode
document.getElementById('geocacheListToggle').style.display = 'none';
document.getElementById('geocacheListSidebar').classList.remove('open');
updateGeocacheVisibility();
// Deactivate edit tools when entering nav mode
// Deactivate edit tools when entering nav mode
Object.values(toolButtons).forEach(btn => btn.classList.remove('active'));
Object.values(toolButtons).forEach(btn => btn.classList.remove('active'));
document.getElementById('reshapeControls').style.display = 'none';
document.getElementById('reshapeControls').style.display = 'none';