-
-
Notifications
You must be signed in to change notification settings - Fork 438
feat: show recently viewed packages/orgs/users on homepage #1594
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
977ef3c
71aaf4a
0c147e4
7991451
f601fee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,42 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { RemovableRef } from '@vueuse/core' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useLocalStorage } from '@vueuse/core' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { computed } from 'vue' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const MAX_RECENT_ITEMS = 5 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const STORAGE_KEY = 'npmx-recent' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export type RecentItemType = 'package' | 'org' | 'user' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface RecentItem { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: RecentItemType | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Canonical identifier: package name, org name (without @), or username */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Display label shown on homepage (e.g. "@nuxt", "~sindresorhus") */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| label: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Unix timestamp (ms) of most recent view */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| viewedAt: number | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let recentRef: RemovableRef<RecentItem[]> | null = null | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function getRecentRef() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!recentRef) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| recentRef = useLocalStorage<RecentItem[]>(STORAGE_KEY, []) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return recentRef | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function useRecentlyViewed() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const items = getRecentRef() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { items: computed(() => items.value) } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function trackRecentView(item: Omit<RecentItem, 'viewedAt'>) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (import.meta.server) return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const items = getRecentRef() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const filtered = items.value.filter( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| existing => !(existing.type === item.type && existing.name === item.name), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| filtered.unshift({ ...item, viewedAt: Date.now() }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| items.value = filtered.slice(0, MAX_RECENT_ITEMS) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the reason for separating If we move
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great feedback, thank you! I'm not a vue/nuxt expert so this is helpful 😃. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard against corrupted localStorage payloads before filtering.
npmx-recentis user‑controlled; if it contains a non‑array value (or an older schema),.filter()will throw and break tracking. Normalise to a safe array before mutation.Suggested fix
export function trackRecentView(item: Omit<RecentItem, 'viewedAt'>) { if (import.meta.server) return const items = getRecentRef() - const filtered = items.value.filter( + const current = Array.isArray(items.value) ? items.value : [] + if (current !== items.value) items.value = current + const filtered = current.filter( existing => !(existing.type === item.type && existing.name === item.name), ) filtered.unshift({ ...item, viewedAt: Date.now() }) items.value = filtered.slice(0, MAX_RECENT_ITEMS) }📝 Committable suggestion