Skip to content

Commit 7991451

Browse files
committed
refactor: simplify useRecentlyViewed composable
1 parent 0c147e4 commit 7991451

File tree

5 files changed

+27
-31
lines changed

5 files changed

+27
-31
lines changed
Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { RemovableRef } from '@vueuse/core'
21
import { useLocalStorage } from '@vueuse/core'
32
import { computed } from 'vue'
43

@@ -17,26 +16,17 @@ export interface RecentItem {
1716
viewedAt: number
1817
}
1918

20-
let recentRef: RemovableRef<RecentItem[]> | null = null
19+
export function useRecentlyViewed() {
20+
const items = useLocalStorage<RecentItem[]>(STORAGE_KEY, [])
2121

22-
function getRecentRef() {
23-
if (!recentRef) {
24-
recentRef = useLocalStorage<RecentItem[]>(STORAGE_KEY, [])
22+
function trackRecentView(item: Omit<RecentItem, 'viewedAt'>) {
23+
if (import.meta.server) return
24+
const filtered = items.value.filter(
25+
existing => !(existing.type === item.type && existing.name === item.name),
26+
)
27+
filtered.unshift({ ...item, viewedAt: Date.now() })
28+
items.value = filtered.slice(0, MAX_RECENT_ITEMS)
2529
}
26-
return recentRef
27-
}
28-
29-
export function useRecentlyViewed() {
30-
const items = getRecentRef()
31-
return { items: computed(() => items.value) }
32-
}
3330

34-
export function trackRecentView(item: Omit<RecentItem, 'viewedAt'>) {
35-
if (import.meta.server) return
36-
const items = getRecentRef()
37-
const filtered = items.value.filter(
38-
existing => !(existing.type === item.type && existing.name === item.name),
39-
)
40-
filtered.unshift({ ...item, viewedAt: Date.now() })
41-
items.value = filtered.slice(0, MAX_RECENT_ITEMS)
31+
return { items: computed(() => items.value), trackRecentView }
4232
}

app/pages/org/[org].vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ watch(orgName, () => {
122122
})
123123
124124
if (import.meta.client) {
125+
const { trackRecentView } = useRecentlyViewed()
125126
watch(
126127
() => [status.value, orgName.value] as const,
127128
([s, name]) => {

app/pages/package/[[org]]/[name].vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,7 @@ onKeyStroke(
645645
const showSkeleton = shallowRef(false)
646646
647647
if (import.meta.client) {
648+
const { trackRecentView } = useRecentlyViewed()
648649
watch(
649650
() => [status.value, packageName.value] as const,
650651
([s, name]) => {

app/pages/~[username]/index.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ watch(username, () => {
120120
})
121121
122122
if (import.meta.client) {
123+
const { trackRecentView } = useRecentlyViewed()
123124
watch(
124125
() => [status.value, username.value] as const,
125126
([s, name]) => {
Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { beforeEach, describe, expect, it, vi } from 'vitest'
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
22
import { ref } from 'vue'
33

44
import type { RecentItem } from '../../../../app/composables/useRecentlyViewed'
@@ -10,44 +10,47 @@ vi.mock('@vueuse/core', () => ({
1010
}))
1111

1212
// Must import after mock is set up
13-
const { trackRecentView, useRecentlyViewed } =
14-
await import('../../../../app/composables/useRecentlyViewed')
13+
const { useRecentlyViewed } = await import('../../../../app/composables/useRecentlyViewed')
1514

1615
describe('useRecentlyViewed', () => {
1716
beforeEach(() => {
1817
storageRef.value = []
1918
})
2019

20+
afterEach(() => {
21+
vi.restoreAllMocks()
22+
})
23+
2124
it('returns an empty list initially', () => {
2225
const { items } = useRecentlyViewed()
2326
expect(items.value).toEqual([])
2427
})
2528

2629
it('adds an item to an empty list', () => {
30+
const { trackRecentView, items } = useRecentlyViewed()
2731
trackRecentView({ type: 'package', name: 'vue', label: 'vue' })
28-
const { items } = useRecentlyViewed()
2932
expect(items.value).toHaveLength(1)
3033
expect(items.value[0]).toMatchObject({ type: 'package', name: 'vue', label: 'vue' })
3134
expect(items.value[0]!.viewedAt).toBeTypeOf('number')
3235
})
3336

3437
it('deduplicates by type and name, bumping to front', () => {
38+
const { trackRecentView, items } = useRecentlyViewed()
3539
trackRecentView({ type: 'package', name: 'vue', label: 'vue' })
3640
trackRecentView({ type: 'package', name: 'react', label: 'react' })
3741
trackRecentView({ type: 'package', name: 'vue', label: 'vue' })
3842

39-
const { items } = useRecentlyViewed()
4043
expect(items.value).toHaveLength(2)
4144
expect(items.value[0]!.name).toBe('vue')
4245
expect(items.value[1]!.name).toBe('react')
4346
})
4447

4548
it('caps at 5 items, evicting the oldest', () => {
49+
const { trackRecentView, items } = useRecentlyViewed()
4650
for (let i = 1; i <= 6; i++) {
4751
trackRecentView({ type: 'package', name: `pkg-${i}`, label: `pkg-${i}` })
4852
}
4953

50-
const { items } = useRecentlyViewed()
5154
expect(items.value).toHaveLength(5)
5255
expect(items.value[0]!.name).toBe('pkg-6')
5356
expect(items.value[4]!.name).toBe('pkg-2')
@@ -56,42 +59,42 @@ describe('useRecentlyViewed', () => {
5659
})
5760

5861
it('allows different entity types to coexist', () => {
62+
const { trackRecentView, items } = useRecentlyViewed()
5963
trackRecentView({ type: 'package', name: 'vue', label: 'vue' })
6064
trackRecentView({ type: 'org', name: 'nuxt', label: '@nuxt' })
6165
trackRecentView({ type: 'user', name: 'sindresorhus', label: '~sindresorhus' })
6266

63-
const { items } = useRecentlyViewed()
6467
expect(items.value).toHaveLength(3)
6568
expect(items.value.map(i => i.type)).toEqual(['user', 'org', 'package'])
6669
})
6770

6871
it('does not deduplicate items with the same name but different type', () => {
72+
const { trackRecentView, items } = useRecentlyViewed()
6973
trackRecentView({ type: 'package', name: 'nuxt', label: 'nuxt' })
7074
trackRecentView({ type: 'org', name: 'nuxt', label: '@nuxt' })
7175

72-
const { items } = useRecentlyViewed()
7376
expect(items.value).toHaveLength(2)
7477
})
7578

7679
it('sets viewedAt on new entries', () => {
80+
const { trackRecentView, items } = useRecentlyViewed()
7781
const before = Date.now()
7882
trackRecentView({ type: 'package', name: 'vue', label: 'vue' })
7983
const after = Date.now()
8084

81-
const { items } = useRecentlyViewed()
8285
expect(items.value[0]!.viewedAt).toBeGreaterThanOrEqual(before)
8386
expect(items.value[0]!.viewedAt).toBeLessThanOrEqual(after)
8487
})
8588

8689
it('updates viewedAt when deduplicating', () => {
90+
const { trackRecentView, items } = useRecentlyViewed()
8791
trackRecentView({ type: 'package', name: 'vue', label: 'vue' })
88-
const firstViewedAt = useRecentlyViewed().items.value[0]!.viewedAt
92+
const firstViewedAt = items.value[0]!.viewedAt
8993

9094
// Small delay to ensure timestamp differs
9195
vi.spyOn(Date, 'now').mockReturnValueOnce(firstViewedAt + 1000)
9296
trackRecentView({ type: 'package', name: 'vue', label: 'vue' })
9397

94-
const { items } = useRecentlyViewed()
9598
expect(items.value[0]!.viewedAt).toBeGreaterThan(firstViewedAt)
9699
})
97100
})

0 commit comments

Comments
 (0)