board: spinner during first load instead of "no cards found"
An empty board on first load blurting "no cards found" was alarming on a board the user knew had cards. Track a cardsLoaded flag that flips on the first /api/cards (meta or full) response — or on a cached-cards hydration — and show a centred spinner placeholder while it's still false. The "no cards found" copy only shows once we've actually heard back and the set is genuinely empty. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
d31e186bd9
commit
41272bd9ce
5 files changed
+27
-7
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
define('APP_NAME', 'hyprcrd');
|
||||
define('APP_VERSION', '1.9.5');
|
||||
define('APP_VERSION', '1.9.6');
|
||||
|
||||
// Database path — writable directory, outside webroot
|
||||
// DB_PATH / UPLOAD_DIR can be pre-defined by the Electron router for app isolation
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hyprcrd",
|
||||
"version": "1.9.5",
|
||||
"version": "1.9.6",
|
||||
"description": "hyprcrd — self-hosted card manager",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
||||
+8
-2
@@ -45,6 +45,7 @@ window.openFindBar = openFindBar;
|
||||
window._reloadAll = async () => {
|
||||
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: '' });
|
||||
}
|
||||
|
||||
@@ -587,9 +587,18 @@ export function renderBoard() {
|
||||
board.style.display = 'flex';
|
||||
board.style.alignItems = 'center';
|
||||
board.style.justifyContent = 'center';
|
||||
board.innerHTML = `<div class="flex flex-col items-center text-center">
|
||||
<i class="ph ph-cards text-4xl text-t-dim mb-4 block"></i>
|
||||
<p class="font-pixel text-sm text-t-dim cursor-blink">no cards found</p></div>`;
|
||||
// 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 = `<div class="flex flex-col items-center text-center">
|
||||
<i class="ph ph-circle-notch text-4xl text-t-dim mb-4 block card-spinner"></i>
|
||||
<p class="font-pixel text-sm text-t-dim">loading…</p></div>`;
|
||||
} else {
|
||||
board.innerHTML = `<div class="flex flex-col items-center text-center">
|
||||
<i class="ph ph-cards text-4xl text-t-dim mb-4 block"></i>
|
||||
<p class="font-pixel text-sm text-t-dim cursor-blink">no cards found</p></div>`;
|
||||
}
|
||||
const fc = $('footer-count');
|
||||
if (fc) fc.textContent = `0 cards · ${state.cards.length} total`;
|
||||
return;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user