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:
thandClaude Opus 4.6 committed 2026-04-15 21:16:45 +02:00
1 parent d31e186bd9
commit 41272bd9ce
5 files changed
+27 -7

No files matched your search

+1 -1
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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: '' });
}
+12 -3
View File
@@ -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;
+5
View File
@@ -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