Browse Source

Add advanced geocache features

Features added:
1. Custom icon colors - Choose any color for geocache icons
2. Secret caches - Set visibility distance (only visible within X meters)
3. Edit existing caches - Change title, icon, color, visibility after creation
4. Geocache list sidebar - View all caches in edit mode, click to navigate
5. Visual indicators for secret caches (purple badge, transparency in edit mode)

Implementation:
- Added color picker with preview in geocache dialog
- Added visibility distance field (0 = always visible)
- Edit button for existing caches in edit mode
- Dynamic visibility updates based on user location in nav mode
- Geocache list sidebar with stats and quick navigation
- All properties sync via WebSocket to other users

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
master
HikeMap User 1 month ago
parent
commit
f9dbc14604
  1. 15
      geocaches.json
  2. 311
      index.html

15
geocaches.json

@ -112,5 +112,20 @@
}
],
"createdAt": 1767140463979
},
{
"id": "gc_1767201872423_vhovqct4m",
"lat": 30.527463067863167,
"lng": -97.83988237380983,
"title": "Pirates booty",
"icon": "pirate",
"messages": [
{
"author": "Captain bibbit.",
"text": "the booty is my butt.",
"timestamp": 1767201911761
}
],
"createdAt": 1767201872423
}
]

311
index.html

@ -616,6 +616,81 @@
display: block;
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-setting-group {
padding: 15px 10px;
@ -974,6 +1049,19 @@
<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>
</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">
<label for="geocacheName">Your Name</label>
<input type="text" id="geocacheName" placeholder="Enter your name" maxlength="50">
@ -986,6 +1074,7 @@
<div class="geocache-dialog-buttons">
<button class="geocache-cancel-btn" id="geocacheCancel">Close</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>
</div>
</div>
@ -996,6 +1085,22 @@
📍 Geocache nearby! Click to view messages.
</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 -->
<div id="navConfirmDialog" class="nav-confirm-dialog" style="display: none;">
<div class="nav-confirm-content">
@ -1142,6 +1247,7 @@
// Geocache variables
let geocaches = []; // Array of { id, lat, lng, messages: [{author, text, timestamp}] }
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 lastGeocacheProximityCheck = 0;
let readGeocaches = JSON.parse(localStorage.getItem('readGeocaches') || '[]'); // Track which caches user has read
@ -1384,6 +1490,11 @@
// Store user location for geocache proximity checks
userLocation = { lat, lng, accuracy };
// Update geocache visibility based on new location
if (navMode) {
updateGeocacheVisibility();
}
// Update or create marker
if (!gpsMarker) {
const myIcon = L.divIcon({
@ -2196,6 +2307,22 @@
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) {
const id = generateGeocacheId();
const geocache = {
@ -2204,6 +2331,8 @@
lng: latlng.lng,
title: '', // Will be set when user submits
icon: 'package-variant', // Default icon
color: '#FFA726', // Default orange color
visibilityDistance: 0, // 0 = always visible, >0 = only visible within that distance
messages: [],
createdAt: Date.now()
};
@ -2214,14 +2343,24 @@
}
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}`);
// Use geocache's custom icon or default
// Use geocache's custom icon and color
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], {
icon: L.divIcon({
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],
iconAnchor: [14, 28]
}),
@ -2287,9 +2426,14 @@
const form = document.getElementById('geocacheForm');
const titleGroup = document.getElementById('geocacheTitleGroup');
const iconGroup = document.getElementById('geocacheIconGroup');
const colorGroup = document.getElementById('geocacheColorGroup');
const visibilityGroup = document.getElementById('geocacheVisibilityGroup');
const titleInput = document.getElementById('geocacheTitleInput');
const iconInput = document.getElementById('geocacheIconInput');
const colorInput = document.getElementById('geocacheColorInput');
const visibilityInput = document.getElementById('geocacheVisibilityInput');
const dialogTitle = document.getElementById('geocacheTitle');
const editBtn = document.getElementById('geocacheEdit');
// Clear previous messages
messagesDiv.innerHTML = '';
@ -2297,17 +2441,31 @@
// Update dialog title
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';
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';
colorInput.value = geocache.color || '#FFA726';
visibilityInput.value = geocache.visibilityDistance || 0;
// Update color preview
updateColorPreview();
} else {
titleGroup.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)
const canViewMessages = !navMode || userDistance <= adminSettings.geocacheRange;
@ -2381,6 +2539,85 @@
function hideGeocacheDialog() {
document.getElementById('geocacheDialog').style.display = 'none';
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) {
@ -2396,11 +2633,15 @@
const messageInput = document.getElementById('geocacheMessage');
const titleInput = document.getElementById('geocacheTitleInput');
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 icon = iconInput.value.trim();
const color = colorInput.value || '#FFA726';
const visibilityDistance = parseInt(visibilityInput.value) || 0;
if (!title) {
alert('Please enter a title for this geocache');
@ -2409,17 +2650,32 @@
currentGeocache.title = title;
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]) {
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;
}
}
}
const name = nameInput.value.trim() || 'Anonymous';
@ -2529,6 +2785,24 @@
document.getElementById('geocacheCancel').addEventListener('click', hideGeocacheDialog);
document.getElementById('geocacheSubmit').addEventListener('click', addGeocacheMessage);
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() {
// Find nearest geocache and open it
if (userLocation) {
@ -3248,6 +3522,10 @@
editContent.classList.add('active');
navMode = false;
// Show geocache list toggle in edit mode
document.getElementById('geocacheListToggle').style.display = 'flex';
updateGeocacheVisibility();
// In edit mode, disable auto-center
if (autoCenterMode) {
autoCenterMode = false;
@ -3272,6 +3550,11 @@
navContent.classList.add('active');
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
Object.values(toolButtons).forEach(btn => btn.classList.remove('active'));
document.getElementById('reshapeControls').style.display = 'none';

Loading…
Cancel
Save