83 lines
2.8 KiB
TypeScript
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 }
|
|
}
|