diff --git a/scripts/check-notes.mjs b/scripts/check-notes.mjs index 8f7bc89..bc42eeb 100644 --- a/scripts/check-notes.mjs +++ b/scripts/check-notes.mjs @@ -132,6 +132,14 @@ if (!notesDirectoryViewSource.includes('/notes/post/${note.slug}/')) { throw new Error('Note detail links must use /notes/post/[slug]/') } +if (!notesDirectoryViewSource.includes('window.history.pushState')) { + throw new Error('Note category tabs must update URLs without a route refresh') +} + +if (!notesDirectoryViewSource.includes('popstate')) { + throw new Error('Note category tabs must support browser back and forward navigation') +} + const noteFiles = readdirSync(notesDirectory) .filter((fileName) => fileName.endsWith('.md')) .sort() diff --git a/src/app/notes/NotesDirectoryView.tsx b/src/app/notes/NotesDirectoryView.tsx index fa92977..9852ad4 100644 --- a/src/app/notes/NotesDirectoryView.tsx +++ b/src/app/notes/NotesDirectoryView.tsx @@ -1,11 +1,16 @@ +'use client' + +import { useCallback, useEffect, useMemo, useState } from 'react' +import type { MouseEvent } from 'react' import Link from 'next/link' import { allNotesCategory, + getNoteCategoryById, getNotesForCategory, noteFilterCategories, type NoteCategory, - type PublicNote, -} from '@/lib/notes' +} from '@/lib/note-categories' +import type { PublicNote } from '@/lib/notes' interface NotesDirectoryViewProps { activeCategory: NoteCategory @@ -28,15 +33,79 @@ const getCategoryHref = (category: NoteCategory) => { return `/notes/${category.id}/` } +const getCategoryFromPathname = (pathname: string) => { + const normalizedPathname = pathname.replace(/\/+$/, '') + + if (normalizedPathname === '/notes') { + return allNotesCategory + } + + const categoryMatch = normalizedPathname.match(/^\/notes\/([^/]+)$/) + + if (!categoryMatch) { + return undefined + } + + return getNoteCategoryById(decodeURIComponent(categoryMatch[1])) +} + export default function NotesDirectoryView({ activeCategory, notes }: NotesDirectoryViewProps) { - const activeNotes = getNotesForCategory(notes, activeCategory) - const categoryTabs = noteFilterCategories.map((category) => ({ - ...category, - count: getNotesForCategory(notes, category).length, - })) - const title = activeCategory.id === allNotesCategory.id + const [activeCategoryId, setActiveCategoryId] = useState(activeCategory.id) + const currentCategory = useMemo(() => ( + noteFilterCategories.find((category) => category.id === activeCategoryId) ?? activeCategory + ), [activeCategory, activeCategoryId]) + const activeNotes = useMemo(() => ( + getNotesForCategory(notes, currentCategory) + ), [currentCategory, notes]) + const categoryTabs = useMemo(() => ( + noteFilterCategories.map((category) => ({ + ...category, + count: getNotesForCategory(notes, category).length, + })) + ), [notes]) + const title = currentCategory.id === allNotesCategory.id ? 'All Notes' - : `${activeCategory.title} Notes` + : `${currentCategory.title} Notes` + + useEffect(() => { + setActiveCategoryId(activeCategory.id) + }, [activeCategory.id]) + + useEffect(() => { + const handlePopState = () => { + const nextCategory = getCategoryFromPathname(window.location.pathname) + + if (nextCategory) { + setActiveCategoryId(nextCategory.id) + } + } + + window.addEventListener('popstate', handlePopState) + + return () => window.removeEventListener('popstate', handlePopState) + }, []) + + useEffect(() => { + document.title = currentCategory.id === allNotesCategory.id + ? 'Notes | dd3ok' + : `${currentCategory.title} Notes | dd3ok` + }, [currentCategory]) + + const handleCategoryClick = useCallback(( + event: MouseEvent, + category: NoteCategory + ) => { + event.preventDefault() + + const nextHref = getCategoryHref(category) + + if (window.location.pathname === nextHref) { + return + } + + setActiveCategoryId(category.id) + window.history.pushState({ notesCategory: category.id }, '', nextHref) + }, []) return (
@@ -57,13 +126,15 @@ export default function NotesDirectoryView({ activeCategory, notes }: NotesDirec