57 lines
1.6 KiB
TypeScript
57 lines
1.6 KiB
TypeScript
export interface ViewedArticle {
|
|
slug: string
|
|
title: string
|
|
viewedAt: number
|
|
}
|
|
|
|
const STORAGE_KEY = 'mathew-recently-viewed'
|
|
const MAX_ENTRIES = 8
|
|
|
|
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 }) {
|
|
/* 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()
|
|
viewed.value = [
|
|
{ ...article, viewedAt: Date.now() },
|
|
...base.filter((v) => v.slug !== article.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 }
|
|
}
|