// European numeric dates: 01.10.2026, 14:32 const nameFormat = new Intl.DateTimeFormat('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) const dayFormat = new Intl.DateTimeFormat('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit' }) /** A note is named by its title, or by when it was created if it has none */ export function notesDocName(doc: Pick): string { return doc.title || (doc.created ? nameFormat.format(new Date(doc.created)) : 'Untitled') } export function notesDocDay(iso: string | null): string { return iso ? dayFormat.format(new Date(iso)) : '' } export function useNotes() { const { data, error, status, refresh } = useFetch('/api/notes/docs', { key: 'notes-docs', default: () => [] }) // Newest first by creation, so editing a note never moves it const docs = computed(() => data.value.toSorted((a, b) => (b.created ?? '').localeCompare(a.created ?? '') || b.id - a.id)) /** Reflects a change in the browser without reloading the list */ function patchSummary(id: number, changes: Partial) { data.value = data.value.map(doc => doc.id === id ? { ...doc, ...changes } : doc) } async function create() { const doc = await $fetch('/api/notes/docs', { method: 'POST' }) data.value = [...data.value, doc] return doc } async function remove(id: number) { await $fetch(`/api/notes/docs/${id}`, { method: 'DELETE' }) data.value = data.value.filter(doc => doc.id !== id) } return { docs, error, status, refresh, patchSummary, create, remove } } /** * `open`: the desktop sidebar (remembered in a cookie). * `drawerOpen`: the list that is pulled down from the top on mobile; it closes once a note is opened. */ export function useNotesSidebar() { const open = useCookie('notes-sidebar', { default: () => true, maxAge: 60 * 60 * 24 * 365 }) const drawerOpen = useState('notes-drawer', () => false) return { open, drawerOpen, toggle: () => { open.value = !open.value }, /** The desktop sidebar, or the drawer below md */ toggleList: () => { if (window.matchMedia('(max-width: 767px)').matches) drawerOpen.value = !drawerOpen.value else open.value = !open.value }, closeOnMobile: () => { drawerOpen.value = false } } } /** Creates a note and opens it; used by the action bar */ export function useNewNote() { const { create } = useNotes() const { closeOnMobile } = useNotesSidebar() const toast = useToast() const creating = useState('notes-creating', () => false) async function createAndOpen() { creating.value = true try { const doc = await create() closeOnMobile() await navigateTo(`/notes/${doc.id}`) } catch (e) { toast.add({ title: 'Couldn\'t create a note', description: errorMessage(e), color: 'error', icon: 'i-lucide-triangle-alert' }) } finally { creating.value = false } } return { creating, createAndOpen } } /** Deletes a note right away; used by the list's trash buttons */ export function useDeleteNote() { const { remove } = useNotes() const route = useRoute() const toast = useToast() return async function deleteNote(doc: NotesDocSummary) { try { // Leave the note first, so its last unsaved edits aren't saved into a deleted row if (String(doc.id) === route.params.id) await navigateTo('/notes') await remove(doc.id) } catch (e) { toast.add({ title: 'Couldn\'t delete the note', description: errorMessage(e), color: 'error', icon: 'i-lucide-triangle-alert' }) } } } const SEARCH_DELAY_MS = 250 /** * The search over title and text. `query` is what's typed; `matches` the ids of matching notes, or null when * not searching. The list is mounted twice (sidebar and drawer), so the searching itself runs once, in notes.vue. */ export function useNotesSearch() { const query = useState('notes-search', () => '') const matches = useState('notes-search-matches', () => null) const searching = useState('notes-searching', () => false) function runSearch() { let timer: ReturnType | undefined let latest = 0 watch(query, (value) => { clearTimeout(timer) const q = value.trim() if (!q) { matches.value = null searching.value = false return } searching.value = true timer = setTimeout(async () => { const request = ++latest try { const docs = await $fetch('/api/notes/docs', { query: { q } }) if (request === latest) matches.value = docs.map(doc => doc.id) } finally { if (request === latest) searching.value = false } }, SEARCH_DELAY_MS) }) } return { query, matches, searching, runSearch } }