Spaces:
Sleeping
Sleeping
File size: 3,742 Bytes
caf3667 a785c24 caf3667 74792a9 caf3667 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | /* utils.js β Global helper utilities for ShipSeg */
function htmlEsc(str) {
return String(str ?? '')
.replace(/&/g,'&')
.replace(/</g,'<')
.replace(/>/g,'>')
.replace(/"/g,'"')
.replace(/'/g,''');
}
function fmtDate(iso) {
if (!iso) return 'β';
try {
let dateStr = iso;
if (typeof dateStr === 'string' && !dateStr.endsWith('Z') && !dateStr.includes('+') && !/[-+]\d{2}:?\d{2}$/.test(dateStr)) {
dateStr = dateStr + 'Z';
}
return new Date(dateStr).toLocaleDateString('en-GB', { day:'numeric', month:'short', year:'2-digit', hour:'2-digit', minute:'2-digit' });
} catch { return iso; }
}
function catLabel(cat) {
return { tonnage:'Tonnage', cargo_vc:'Cargo VC', cargo_tc:'Cargo TC', unknown:'Unknown' }[cat] || cat;
}
function debounce(fn, ms) {
let t;
return function(...args) { clearTimeout(t); t = setTimeout(() => fn.apply(this, args), ms); };
}
// ββ Toast βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
const Toast = {
show(msg, type = 'info', duration = 3500) {
const icons = { success: 'β', error: 'β', info: 'βΉ', warning: 'β ' };
const el = document.createElement('div');
el.className = `toast ${type}`;
el.innerHTML = `<span class="toast-icon">${icons[type]||'βΉ'}</span><span class="toast-msg">${htmlEsc(msg)}</span>`;
const container = document.getElementById('toast-container');
container.appendChild(el);
setTimeout(() => {
el.style.opacity = '0';
el.style.transition = 'opacity 0.3s';
setTimeout(() => el.remove(), 300);
}, duration);
},
};
// ββ Shared edit modal βββββββββββββββββββββββββββββββββββββββββββββ
function showEditModal(title, fields, onSave) {
const existing = document.getElementById('edit-modal-overlay');
if (existing) existing.remove();
const overlay = document.createElement('div');
overlay.id = 'edit-modal-overlay';
overlay.className = 'modal-overlay open';
overlay.innerHTML = `
<div class="modal" style="max-width:560px">
<div class="modal-header">
<h2 class="modal-title">${htmlEsc(title)}</h2>
<button class="modal-close" onclick="document.getElementById('edit-modal-overlay').remove()">β</button>
</div>
<div class="modal-body">
${fields.map(([key, label, val]) => `
<div class="form-group">
<label class="form-label" for="edit-${key}">${htmlEsc(label)}</label>
<input type="text" id="edit-${key}" class="form-input"
value="${htmlEsc(val||'')}" data-key="${key}" />
</div>`).join('')}
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="document.getElementById('edit-modal-overlay').remove()">Cancel</button>
<button class="btn btn-primary" id="edit-save-btn">Save Changes</button>
</div>
</div>`;
overlay.onclick = e => { if (e.target === overlay) overlay.remove(); };
document.body.appendChild(overlay);
document.getElementById('edit-save-btn').onclick = async () => {
const form = {};
overlay.querySelectorAll('[data-key]').forEach(el => {
form[el.dataset.key] = el.value.trim() || null;
});
const btn = document.getElementById('edit-save-btn');
btn.textContent = 'Savingβ¦';
btn.disabled = true;
try {
await onSave(form);
overlay.remove();
} catch(e) {
Toast.show('Save failed: ' + e.message, 'error');
btn.textContent = 'Save Changes';
btn.disabled = false;
}
};
}
|