2026-05-20 10:50:30 +00:00

278 lines
10 KiB
JavaScript

document.addEventListener('DOMContentLoaded', function () {
const sidebarToggle = document.querySelector('.sidebar-toggle');
const sidebarOverlay = document.querySelector('.sidebar-overlay');
const sidebarClose = document.querySelector('.sidebar-close');
const modeToggles = document.querySelectorAll('.mode-toggle');
const geolocateButtons = document.querySelectorAll('[data-geolocate-btn]');
const body = document.body;
const collapseKey = 'hk_nav_collapsed';
const themeKey = 'hk_theme';
const syncThemeToggleLabels = function (dark) {
modeToggles.forEach(function (toggle) {
const icon = toggle.querySelector('.theme-icon');
const label = toggle.querySelector('.theme-label');
const text = dark ? 'Light Mode' : 'Dark Mode';
if (icon) {
icon.textContent = dark ? '☀️' : '🌙';
}
if (label) {
label.textContent = dark ? 'Light' : 'Dark';
} else {
toggle.textContent = text;
}
toggle.setAttribute('aria-label', dark ? 'Switch to light mode' : 'Switch to dark mode');
});
};
const setCollapsed = function (collapsed) {
body.classList.toggle('nav-collapsed', collapsed);
localStorage.setItem(collapseKey, collapsed ? '1' : '0');
};
const setTheme = function (theme) {
const dark = theme === 'dark';
body.classList.toggle('theme-dark', dark);
syncThemeToggleLabels(dark);
localStorage.setItem(themeKey, dark ? 'dark' : 'light');
};
const savedTheme = localStorage.getItem(themeKey) || 'light';
setTheme(savedTheme);
if (window.innerWidth > 1024) {
setCollapsed(localStorage.getItem(collapseKey) === '1');
}
if (modeToggles.length) {
modeToggles.forEach(function (toggle) {
toggle.addEventListener('click', function () {
setTheme(body.classList.contains('theme-dark') ? 'light' : 'dark');
});
});
}
if (sidebarToggle) {
sidebarToggle.addEventListener('click', function () {
if (window.innerWidth <= 1024) {
body.classList.toggle('sidebar-open');
return;
}
setCollapsed(!body.classList.contains('nav-collapsed'));
});
}
if (sidebarOverlay) {
sidebarOverlay.addEventListener('click', function () {
body.classList.remove('sidebar-open');
});
}
if (sidebarClose) {
sidebarClose.addEventListener('click', function () {
body.classList.remove('sidebar-open');
});
}
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape') {
body.classList.remove('sidebar-open');
}
});
const revealElements = document.querySelectorAll('.reveal');
if (revealElements.length) {
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
observer.unobserve(entry.target);
}
});
},
{
threshold: 0.12,
rootMargin: '0px 0px -40px 0px',
}
);
revealElements.forEach(function (element) {
observer.observe(element);
});
} else {
revealElements.forEach(function (element) {
element.classList.add('is-visible');
});
}
}
const findLocationField = function (container, names) {
for (let index = 0; index < names.length; index += 1) {
const field = container.querySelector('[name="' + names[index] + '"]');
if (field) {
return field;
}
}
return null;
};
const setStatus = function (container, type, message) {
const statusNode = container.querySelector('[data-location-status]');
if (!statusNode) {
return;
}
statusNode.textContent = message;
statusNode.classList.remove('is-success', 'is-error');
if (type === 'success') {
statusNode.classList.add('is-success');
}
if (type === 'error') {
statusNode.classList.add('is-error');
}
};
const setFieldValue = function (field, value, overwriteWhenFilled) {
if (!field) {
return;
}
if (!overwriteWhenFilled && field.value) {
return;
}
field.value = value || '';
};
const buildAddressText = function (address) {
const roadLine = [address.house_number, address.road].filter(Boolean).join(' ').trim();
const areaLine = [
address.suburb,
address.neighbourhood,
address.city || address.town || address.village || address.municipality,
address.state,
address.postcode,
address.country,
]
.filter(Boolean)
.filter(function (value, index, values) {
return values.indexOf(value) === index;
})
.join(', ')
.trim();
return [roadLine, areaLine].filter(Boolean).join('\n').trim();
};
const buildLocationLabel = function (address) {
return [
address.suburb || address.neighbourhood || address.hamlet,
address.city || address.town || address.village || address.municipality,
address.state,
]
.filter(Boolean)
.filter(function (value, index, values) {
return values.indexOf(value) === index;
})
.join(', ')
.trim();
};
const reverseGeocode = async function (latitude, longitude) {
const url = 'https://nominatim.openstreetmap.org/reverse?format=jsonv2&addressdetails=1&lat=' + encodeURIComponent(latitude) + '&lon=' + encodeURIComponent(longitude);
const response = await fetch(url, {
headers: {
Accept: 'application/json',
},
});
if (!response.ok) {
throw new Error('Reverse geocoding is temporarily unavailable.');
}
return response.json();
};
if (geolocateButtons.length) {
geolocateButtons.forEach(function (button) {
button.addEventListener('click', function () {
const container = button.closest('[data-location-form]') || button.closest('form') || document;
const latitudeField = findLocationField(container, ['latitude']);
const longitudeField = findLocationField(container, ['longitude']);
const accuracyField = findLocationField(container, ['location_accuracy_m']);
const labelField = findLocationField(container, ['location_label']);
const addressField = findLocationField(container, ['default_address', 'address']);
if (!navigator.geolocation) {
setStatus(container, 'error', 'Your browser does not support GPS access. You can still enter your address manually.');
return;
}
button.disabled = true;
setStatus(container, '', 'Requesting GPS access from your device…');
navigator.geolocation.getCurrentPosition(
async function (position) {
const latitude = Number(position.coords.latitude).toFixed(6);
const longitude = Number(position.coords.longitude).toFixed(6);
const accuracy = Number(position.coords.accuracy || 0).toFixed(2);
if (latitudeField) {
latitudeField.value = latitude;
}
if (longitudeField) {
longitudeField.value = longitude;
}
if (accuracyField) {
accuracyField.value = accuracy;
}
try {
const payload = await reverseGeocode(latitude, longitude);
const address = payload.address || {};
const suggestedAddress = buildAddressText(address);
const locationLabel = buildLocationLabel(address);
setFieldValue(labelField, locationLabel, false);
setFieldValue(addressField, suggestedAddress || payload.display_name || '', false);
setStatus(
container,
'success',
'GPS captured successfully. Accuracy ±' + Math.round(Number(accuracy)) + ' meters. Please review the address before saving.'
);
} catch (error) {
setStatus(
container,
'success',
'GPS captured successfully. Exact coordinates were saved, but the address suggestion could not be loaded right now.'
);
} finally {
button.disabled = false;
}
},
function (error) {
let message = 'Unable to get your location. Please enter your address manually.';
if (error.code === error.PERMISSION_DENIED) {
message = 'Location permission was denied. You can still continue with a manual address.';
} else if (error.code === error.TIMEOUT) {
message = 'Location request timed out. Please retry or continue manually.';
}
setStatus(container, 'error', message);
button.disabled = false;
},
{
enableHighAccuracy: true,
timeout: 15000,
maximumAge: 0,
}
);
});
});
}
});