mirror of
https://github.com/alexhopeoconnor/bom-local-service.git
synced 2026-10-03 21:21:59 +10:00
185 lines
5.8 KiB
JavaScript
185 lines
5.8 KiB
JavaScript
// Utility functions
|
|
|
|
// Cookie management
|
|
export function setCookie(name, value, days = 365) {
|
|
const expires = new Date();
|
|
expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000));
|
|
document.cookie = `${name}=${encodeURIComponent(JSON.stringify(value))};expires=${expires.toUTCString()};path=/`;
|
|
}
|
|
|
|
export function getCookie(name) {
|
|
const nameEQ = name + "=";
|
|
const ca = document.cookie.split(';');
|
|
for (let i = 0; i < ca.length; i++) {
|
|
let c = ca[i];
|
|
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
|
|
if (c.indexOf(nameEQ) === 0) {
|
|
try {
|
|
return JSON.parse(decodeURIComponent(c.substring(nameEQ.length, c.length)));
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Date formatting utilities
|
|
export function formatDate(dateString) {
|
|
if (!dateString) return '-';
|
|
const date = new Date(dateString);
|
|
return date.toLocaleString('en-AU', {
|
|
timeZone: 'Australia/Brisbane',
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit'
|
|
});
|
|
}
|
|
|
|
export function getRelativeTime(dateString) {
|
|
if (!dateString) return '';
|
|
const date = new Date(dateString);
|
|
const now = new Date();
|
|
const diffMs = date - now;
|
|
const diffMins = Math.floor(diffMs / 60000);
|
|
const diffSecs = Math.floor((diffMs % 60000) / 1000);
|
|
|
|
if (diffMs < 0) {
|
|
return Math.abs(diffMins) + ' minute(s) ago';
|
|
} else if (diffMins > 0) {
|
|
return 'in ' + diffMins + ' minute(s)';
|
|
} else {
|
|
return 'in ' + diffSecs + ' second(s)';
|
|
}
|
|
}
|
|
|
|
export function getMinutesAgo(dateString) {
|
|
if (!dateString) return null;
|
|
|
|
// Handle both string and Date object inputs
|
|
let date;
|
|
if (typeof dateString === 'string') {
|
|
date = new Date(dateString);
|
|
} else if (dateString instanceof Date) {
|
|
date = dateString;
|
|
} else {
|
|
console.warn('getMinutesAgo: Expected string or Date, got:', typeof dateString, dateString);
|
|
return null;
|
|
}
|
|
|
|
// Check if date is valid
|
|
if (isNaN(date.getTime())) {
|
|
console.warn('getMinutesAgo: Invalid date:', dateString, 'parsed as:', date);
|
|
return null;
|
|
}
|
|
|
|
// Calculate difference in milliseconds
|
|
// Date.now() is UTC-based, date.getTime() is UTC-based, so this should be correct
|
|
const now = Date.now();
|
|
const dateTime = date.getTime();
|
|
const diffMs = now - dateTime;
|
|
|
|
// Convert to minutes and round
|
|
const minutes = Math.round(diffMs / 60000);
|
|
|
|
// If negative (future date), return null
|
|
if (minutes < 0) {
|
|
console.warn('getMinutesAgo: Future date detected:', dateString, 'parsed as:', date.toISOString());
|
|
return null;
|
|
}
|
|
|
|
// Safeguard against unreasonably large values (likely parsing errors)
|
|
// But don't cap at 60 - radar frames can legitimately be older in some cases
|
|
if (minutes > 100000) { // e.g., more than ~69 days ago
|
|
console.warn('getMinutesAgo: Unreasonably large minutes value:', minutes, 'for date:', dateString, 'parsed as:', date.toISOString());
|
|
return null;
|
|
}
|
|
|
|
return minutes;
|
|
}
|
|
|
|
// Format frame time for display (consolidates duplicate logic)
|
|
export function formatFrameTime(frame, isExtendedMode) {
|
|
if (!frame.absoluteObservationTime) return '-';
|
|
|
|
if (isExtendedMode) {
|
|
return formatDate(frame.absoluteObservationTime);
|
|
} else {
|
|
const minutesAgo = getMinutesAgo(frame.absoluteObservationTime);
|
|
// If minutesAgo is null (invalid date or edge case), fall back to formatted date
|
|
if (minutesAgo === null) {
|
|
return formatDate(frame.absoluteObservationTime);
|
|
}
|
|
return `${minutesAgo} min ago`;
|
|
}
|
|
}
|
|
|
|
// Network error detection (consolidates duplicate checks)
|
|
export function isNetworkError(error) {
|
|
return error.name === 'TypeError' ||
|
|
error.name === 'AbortError' ||
|
|
error.message.includes('fetch') ||
|
|
error.message.includes('network') ||
|
|
error.message.includes('Failed to fetch') ||
|
|
error.message === 'Network error';
|
|
}
|
|
|
|
export function isNetworkErrorResponse(response) {
|
|
return response.status === 0 || response.status >= 500;
|
|
}
|
|
|
|
// DOM helpers
|
|
export function getElement(id) {
|
|
return document.getElementById(id);
|
|
}
|
|
|
|
export function setElementText(id, text) {
|
|
const el = getElement(id);
|
|
if (el) el.textContent = text;
|
|
}
|
|
|
|
export function setElementHTML(id, html) {
|
|
const el = getElement(id);
|
|
if (el) el.innerHTML = html;
|
|
}
|
|
|
|
// Create cache status data object (consolidates duplicate creation)
|
|
export function createCacheStatusData(errorData) {
|
|
const details = errorData.details || errorData;
|
|
const isUpdating = details.updateTriggered === true ||
|
|
(errorData.message && errorData.message.includes('in progress')) ||
|
|
(details.statusMessage && details.statusMessage.includes('in progress'));
|
|
|
|
return {
|
|
frames: [],
|
|
isUpdating: isUpdating,
|
|
cacheIsValid: details.cacheIsValid || false,
|
|
cacheExpiresAt: details.cacheExpiresAt || null,
|
|
nextUpdateTime: details.nextUpdateTime || null
|
|
};
|
|
}
|
|
|
|
// Trigger background cache refresh (consolidates duplicate calls)
|
|
export function triggerBackgroundRefresh(refreshEndpoint) {
|
|
if (refreshEndpoint) {
|
|
fetch(refreshEndpoint, {
|
|
method: 'POST',
|
|
signal: AbortSignal.timeout(5000)
|
|
}).catch(err => {
|
|
console.debug('Background cache refresh trigger failed (non-critical):', err);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Preload all frame images to prevent jiggle
|
|
export function preloadImages(frames) {
|
|
frames.forEach(frame => {
|
|
const img = new Image();
|
|
img.src = frame.imageUrl;
|
|
});
|
|
}
|
|
|