diff --git a/config.php b/config.php index ea6a849..616d4cb 100644 --- a/config.php +++ b/config.php @@ -1,7 +1,7 @@ { await loadBuckets(); state.cards = await api('api/cards') || []; + state.cardsLoaded = true; renderBoard(); refreshCalendar(); // Let plugins / modules react to a full card reload (detail view checks @@ -86,10 +87,14 @@ function setCachedCards(cards) { } async function loadCards() { - // Phase 0: Show cached cards instantly (from previous session) + // Phase 0: Show cached cards instantly (from previous session). + // A hit here is enough to flip cardsLoaded — fresh meta will + // overwrite state.cards a moment later, but the spinner→content + // transition already happened. const cached = getCachedCards(); if (cached.length) { state.cards = cached.map(c => ({ ...c, body: null })); + state.cardsLoaded = true; updateSortUI(); renderBoard(); } @@ -97,6 +102,7 @@ async function loadCards() { // Phase 1: Load metadata → update board with fresh titles/tags/dates const meta = await api('api/cards/meta'); if (!meta) return; + state.cardsLoaded = true; state.cards = meta.map(c => ({ ...c, body: null })); setCachedCards(meta); updateSortUI(); @@ -158,7 +164,7 @@ importFile?.addEventListener('change', async e => { if (!res.ok) throw new Error(text || `HTTP ${res.status}`); const data = JSON.parse(text); if (data.error) throw new Error(data.error); - await loadBuckets(); state.cards = await api('api/cards'); renderBoard(); + await loadBuckets(); state.cards = await api('api/cards'); state.cardsLoaded = true; renderBoard(); } catch (err) { await showDialog({ title: 'import failed', message: err.message || 'Unknown error', danger: true, okLabel: 'OK', cancelLabel: '' }); } diff --git a/public/js/modules/board.js b/public/js/modules/board.js index a27d804..53a7719 100644 --- a/public/js/modules/board.js +++ b/public/js/modules/board.js @@ -587,9 +587,18 @@ export function renderBoard() { board.style.display = 'flex'; board.style.alignItems = 'center'; board.style.justifyContent = 'center'; - board.innerHTML = `
- -
`; + // Distinguish "genuinely empty" from "haven't heard back yet". + // Blurting "no cards found" during the initial load is scary + // on boards the user knows aren't empty. + if (!state.cardsLoaded) { + board.innerHTML = `
+ +

loading…

`; + } else { + board.innerHTML = `
+ +
`; + } const fc = $('footer-count'); if (fc) fc.textContent = `0 cards · ${state.cards.length} total`; return; diff --git a/public/js/modules/state.js b/public/js/modules/state.js index cb60024..9c1e83b 100644 --- a/public/js/modules/state.js +++ b/public/js/modules/state.js @@ -6,6 +6,11 @@ export const state = { // Data cards: [], buckets: [], + // True once we've got a real response from api/cards (or the + // cached-cards hydration). Until then the board shows a spinner + // instead of the "no cards found" placeholder — an empty board on + // first load is an honest "still fetching" state, not a real empty. + cardsLoaded: false, // Filters & view — activeBucket is persisted across reloads via // localStorage. Numeric ids are stringified; the special filters