98 lines
2.8 KiB
JavaScript
98 lines
2.8 KiB
JavaScript
// Shared utility functions
|
|
|
|
export function escHtml(str) {
|
|
return String(str ?? '').replace(/[&<>"']/g, c => ({
|
|
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
|
})[c]);
|
|
}
|
|
|
|
export function formatDate(iso) {
|
|
if (!iso) return '—';
|
|
try {
|
|
return new Date(iso).toLocaleDateString('en-US', {
|
|
year: 'numeric', month: 'short', day: 'numeric'
|
|
});
|
|
} catch { return iso.slice(0, 10); }
|
|
}
|
|
|
|
export function formatDateTime(iso) {
|
|
if (!iso) return '—';
|
|
try {
|
|
return new Date(iso).toLocaleString('en-US', {
|
|
year: 'numeric', month: 'short', day: 'numeric',
|
|
hour: '2-digit', minute: '2-digit'
|
|
});
|
|
} catch { return iso.slice(0, 19).replace('T', ' '); }
|
|
}
|
|
|
|
export function formatRelative(iso) {
|
|
if (!iso) return '—';
|
|
const diff = Date.now() - new Date(iso).getTime();
|
|
const m = Math.floor(diff / 60000);
|
|
if (m < 1) return 'just now';
|
|
if (m < 60) return `${m}m ago`;
|
|
const h = Math.floor(m / 60);
|
|
if (h < 24) return `${h}h ago`;
|
|
const d = Math.floor(h / 24);
|
|
if (d < 7) return `${d}d ago`;
|
|
return formatDate(iso);
|
|
}
|
|
|
|
export function debounce(fn, ms = 300) {
|
|
let timer;
|
|
return (...args) => {
|
|
clearTimeout(timer);
|
|
timer = setTimeout(() => fn(...args), ms);
|
|
};
|
|
}
|
|
|
|
export function uuid() {
|
|
return crypto.randomUUID
|
|
? crypto.randomUUID()
|
|
: 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
|
const r = Math.random() * 16 | 0;
|
|
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
|
});
|
|
}
|
|
|
|
// Truncate a string to maxLen chars, appending ellipsis if needed
|
|
export function truncate(str, maxLen = 40) {
|
|
if (!str) return '';
|
|
return str.length > maxLen ? str.slice(0, maxLen - 1) + '…' : str;
|
|
}
|
|
|
|
// First letter of a name for avatar initials
|
|
export function initials(name) {
|
|
if (!name) return '?';
|
|
const parts = name.trim().split(/\s+/);
|
|
return parts.length >= 2
|
|
? (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
|
|
: name.slice(0, 2).toUpperCase();
|
|
}
|
|
|
|
// Download a string as a file
|
|
export function downloadText(filename, content, type = 'text/plain') {
|
|
const blob = new Blob([content], { type });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url; a.download = filename;
|
|
document.body.appendChild(a); a.click();
|
|
document.body.removeChild(a); URL.revokeObjectURL(url);
|
|
}
|
|
|
|
// Convert array of objects to CSV and download
|
|
export function downloadCsv(filename, rows) {
|
|
if (!rows.length) return;
|
|
const headers = Object.keys(rows[0]);
|
|
const csv = [
|
|
headers.join(','),
|
|
...rows.map(r => headers.map(h => JSON.stringify(r[h] ?? '')).join(','))
|
|
].join('\n');
|
|
downloadText(filename, csv, 'text/csv');
|
|
}
|
|
|
|
// Shorten a SHA-256 hash for display
|
|
export function shortHash(hash, len = 8) {
|
|
return hash ? hash.slice(0, len) : '—';
|
|
}
|