Files
mathew/frontend/app/composables/useRecentlyViewed.ts
2026-10-01 02:29:21 +02:00

83 lines
2.8 KiB
TypeScript

export interface ViewedArticle {
/** the article read, by its identity — the canonical version's slug. An
* entry written before versions had addresses of their own may hold a
* document's own slug, which the reader still resolves. */
slug: string
title: string
/** the language it was last read in, so a jump returns to that version.
* Absent from entries written when one address stood for the whole article */
lang?: string
viewedAt: number
}
const STORAGE_KEY = 'mathew-recently-viewed'
const MAX_ENTRIES = 8
/** where a jump leads: the version the article was last read in, or — for an
* entry that recorded no language — the short address, which the reader turns
* onto whichever version the slug speaks */
export function viewedHref(viewed: Pick<ViewedArticle, 'slug' | 'lang'>): string {
return viewed.lang
? versionHref(viewed.slug, viewed.lang)
: `/wiki/${encodeURIComponent(viewed.slug)}`
}
let loaded = false
function readStored(): ViewedArticle[] {
try {
const raw = localStorage.getItem(STORAGE_KEY)
return raw ? (JSON.parse(raw) as ViewedArticle[]) : []
} catch {
/* ignore corrupt storage */
return []
}
}
/**
* Tracks which articles the visitor has opened, in `localStorage`, so the
* sidebar can offer a "recently viewed" list. Shared across pages via
* `useState`, so it updates live as you browse without extra fetches.
*/
export function useRecentlyViewed() {
const viewed = useState<ViewedArticle[]>('recently-viewed', () => [])
/* read the saved history once, after mount, so the server render and the
client hydration both start from an empty list */
onMounted(() => {
if (loaded) return
loaded = true
const stored = readStored()
if (stored.length) viewed.value = stored
})
function record(article: {
slug: string
title: string
baseSlug?: string | null
language?: string
}) {
/* filed by the article's identity and turned to the language it was read
in: an article read in two languages is still one article remembered
once, at the version it was last opened at */
const entry = {
slug: identityOf(article),
lang: articleLanguage(article),
title: article.title,
}
/* merge with what's already saved so recording before the list has
loaded (e.g. deep-linking straight to an article) can't clobber it */
const base = viewed.value.length ? viewed.value : readStored()
const fresh = { ...entry, viewedAt: Date.now() }
viewed.value = [fresh, ...base.filter((v) => v.slug !== entry.slug)].slice(0, MAX_ENTRIES)
loaded = true
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(viewed.value))
} catch {
/* private mode etc. — history just won't persist */
}
}
return { viewed, record }
}