diff --git a/frontend/src/composables/useNoteList.ts b/frontend/src/composables/useNoteList.ts new file mode 100644 index 0000000..5093eed --- /dev/null +++ b/frontend/src/composables/useNoteList.ts @@ -0,0 +1,38 @@ +import { ref, type Ref } from "vue"; +import type { Note } from "../stores/notes"; + +// The load-a-list-of-notes scaffold every recall view (Search, Timeline, Reminders) +// hand-rolled: an items ref + loading/error flags + a load() that fills them and never +// leaves a half-state. The view supplies just the fetcher; wire the returned refs into +// and pass `items` to /cards. +// +// `fetcher` returns the notes to show (or [] — e.g. an empty search box). A thrown +// ApiError's `.error` becomes the message; otherwise `fallbackError` is shown. +export function useNoteList( + fetcher: () => Promise, + fallbackError = "Couldn't load your notes.", +): { + items: Ref; + loading: Ref; + error: Ref; + load: () => Promise; +} { + const items = ref([]); + const loading = ref(false); + const error = ref(""); + + async function load(): Promise { + loading.value = true; + error.value = ""; + try { + items.value = await fetcher(); + } catch (e) { + error.value = (e as { error?: string }).error ?? fallbackError; + items.value = []; + } finally { + loading.value = false; + } + } + + return { items, loading, error, load }; +} diff --git a/frontend/src/stores/reminders.ts b/frontend/src/stores/reminders.ts index 95f3e5b..087f59b 100644 --- a/frontend/src/stores/reminders.ts +++ b/frontend/src/stores/reminders.ts @@ -20,11 +20,17 @@ export const useReminderStore = defineStore("reminders", () => { let timer: ReturnType | undefined; let primed = false; + // Single owner of the reminders endpoint — the RemindersView reads its list through + // this too, so the URL + response shape live in one place. + async function fetchReminders(): Promise { + return (await api.get<{ notes: Note[] }>("/api/notes/reminders")).notes; + } + async function check(): Promise { const ui = useUiStore(); let notes: Note[]; try { - notes = (await api.get<{ notes: Note[] }>("/api/notes/reminders")).notes; + notes = await fetchReminders(); } catch { return; } @@ -66,5 +72,5 @@ export const useReminderStore = defineStore("reminders", () => { osEnabled.value = (await Notification.requestPermission()) === "granted"; } - return { osSupported, osEnabled, start, stop, enableOs }; + return { osSupported, osEnabled, start, stop, enableOs, fetchReminders }; }); diff --git a/frontend/src/views/GraphView.vue b/frontend/src/views/GraphView.vue index cc58899..00821cd 100644 --- a/frontend/src/views/GraphView.vue +++ b/frontend/src/views/GraphView.vue @@ -2,8 +2,9 @@ import { computed, onBeforeUnmount, onMounted, ref } from "vue"; import { useRouter } from "vue-router"; import { api } from "../api/client"; -import { useNotesStore, type Note } from "../stores/notes"; import { NOTE_NODE_FILL, type NoteColor } from "../notes/colors"; +import { useNoteEditor } from "../composables/useNoteEditor"; +import AsyncState from "../components/AsyncState.vue"; import NoteEditor from "../components/NoteEditor.vue"; type NodeKind = "note" | "label"; @@ -29,12 +30,12 @@ const WIDTH = 1000; const HEIGHT = 700; const router = useRouter(); -const notes = useNotesStore(); const allNodes = ref([]); const edges = ref([]); const loading = ref(true); const error = ref(""); -const editing = ref(null); +// Editor host: resolve a node id against the store, else fetch (shared controller). +const { editing, close: closeEditor, navigate } = useNoteEditor(); // Unlinked notes float in the space by default — the graph is a gentle overview, // not a links-only surface. Label hubs are on by default so tags cluster notes. const showAll = ref(true); @@ -263,7 +264,7 @@ function clickNode(n: GNode) { void router.push(`/label/${n.labelId}`); return; } - void openNode(n); + void navigate(n.id); } function onWheel(e: WheelEvent) { @@ -293,18 +294,6 @@ function toggleLabels() { reheat(); } -async function openNode(n: GNode) { - const found = notes.items.find((x) => x.id === n.id); - editing.value = found ?? (await notes.fetchOne(n.id)); -} -function closeEditor() { - editing.value = null; -} -async function onNavigate(id: string) { - const found = notes.items.find((x) => x.id === id); - editing.value = found ?? (await notes.fetchOne(id)); -} - onMounted(loadGraph); onBeforeUnmount(() => { cancelAnimationFrame(raf); @@ -336,21 +325,13 @@ onBeforeUnmount(() => { -
Loading graph…
- -
-

Couldn't load the graph

-

{{ error }}

- -
- -
+ +

No notes yet

Create notes and link them with @@ -430,9 +411,10 @@ onBeforeUnmount(() => {

+
diff --git a/frontend/src/views/RemindersView.vue b/frontend/src/views/RemindersView.vue index 04e847b..42f539c 100644 --- a/frontend/src/views/RemindersView.vue +++ b/frontend/src/views/RemindersView.vue @@ -1,42 +1,23 @@