diff --git a/frontend/src/components/ScrollSentinel.vue b/frontend/src/components/ScrollSentinel.vue new file mode 100644 index 00000000..e5b848c9 --- /dev/null +++ b/frontend/src/components/ScrollSentinel.vue @@ -0,0 +1,96 @@ + + + + + + + Couldn’t load more. + Try again + + + + Loading more… + + + + + diff --git a/frontend/src/views/FamilyView.vue b/frontend/src/views/FamilyView.vue index d4fac26f..7c039b82 100644 --- a/frontend/src/views/FamilyView.vue +++ b/frontend/src/views/FamilyView.vue @@ -19,6 +19,7 @@ import { type FamilyIdea, type IdeaSnapshot, type IdeaStatus, } from "@/api/family"; import FamilyAdoptionMatrix from "@/components/FamilyAdoptionMatrix.vue"; +import ScrollSentinel from "@/components/ScrollSentinel.vue"; import { useToastStore } from "@/stores/toast"; import { fmtStamp } from "@/utils/dateFormat"; import { recordHref } from "@/utils/recordHref"; @@ -36,6 +37,8 @@ const loading = ref(true); const failed = ref(false); const moreDecisions = ref(false); const loadingMore = ref(false); +const moreFailed = ref(false); +const decisionOffset = ref(0); // One pending reversal at a time: which row, which act, and its reason. const pending = ref<{ kind: "undo" | "retire"; id: number } | null>(null); @@ -63,7 +66,9 @@ async function load() { criteria.value = ideaData.criteria; conflictOrder.value = ideaData.conflict_order ?? []; decisions.value = log; + decisionOffset.value = log.length; moreDecisions.value = log.length === PAGE; + moreFailed.value = false; } catch { // Said out loud: a log that failed to load must not read as "nothing // has been decided". @@ -73,14 +78,20 @@ async function load() { } } +// Older decisions load as the log is scrolled. The log is offset-paged, so a +// decision recorded since the first page shifts the rest down by one; a row +// already shown is skipped, and the offset still counts what the server sent. async function loadMore() { loadingMore.value = true; + moreFailed.value = false; try { - const next = await listFamilyDecisions(PAGE, decisions.value.length); - decisions.value.push(...next); + const next = await listFamilyDecisions(PAGE, decisionOffset.value); + const shown = new Set(decisions.value.map((d) => d.id)); + decisions.value.push(...next.filter((d) => !shown.has(d.id))); + decisionOffset.value += next.length; moreDecisions.value = next.length === PAGE; - } catch (e: unknown) { - toast.show(apiErrorMessage(e, "Could not load more decisions"), "error"); + } catch { + moreFailed.value = true; } finally { loadingMore.value = false; } @@ -343,9 +354,12 @@ onMounted(load); - - {{ loadingMore ? "Loading…" : "Older decisions" }} - + diff --git a/frontend/src/views/KnowledgeView.vue b/frontend/src/views/KnowledgeView.vue index 4a48021e..9651c818 100644 --- a/frontend/src/views/KnowledgeView.vue +++ b/frontend/src/views/KnowledgeView.vue @@ -1,5 +1,5 @@ @@ -583,10 +580,7 @@ onUnmounted(() => { + to two different questions on one screen is neither. --> Loading… @@ -675,10 +669,12 @@ onUnmounted(() => { - - - Loading… - + @@ -1115,19 +1111,6 @@ onUnmounted(() => { opacity: 0.85; } -/* ── Sentinel ────────────────────────────────────────────── */ -.scroll-sentinel { - grid-column: 1 / -1; - height: 40px; - display: flex; - align-items: center; - justify-content: center; -} -.sentinel-loading { - font-size: 0.8rem; - color: var(--fs-text-tertiary); -} - /* ── Graph panel ─────────────────────────────────────────── */ .graph-panel { width: 500px;
+ Couldn’t load more. + Try again +
Loading more…