From d8dd0db5913cd5bf8ec62c827a6eab211a9db9e1 Mon Sep 17 00:00:00 2001 From: Florian Heuberger Date: Fri, 30 Jan 2026 00:34:35 +0100 Subject: [PATCH 1/4] feat: add bookmark buttons and logic --- app/components/AppHeader.vue | 6 +- app/components/BookmarkButton.vue | 37 +++++++ app/components/HeaderBookmarksDropdown.vue | 112 +++++++++++++++++++++ app/composables/useBookmarks.ts | 92 +++++++++++++++++ app/pages/[...package].vue | 5 +- i18n/locales/de-DE.json | 11 ++ i18n/locales/en.json | 11 ++ i18n/locales/es.json | 11 ++ i18n/locales/fr-FR.json | 11 ++ i18n/locales/it-IT.json | 11 ++ i18n/locales/ja-JP.json | 11 ++ i18n/locales/zh-CN.json | 11 ++ 12 files changed, 327 insertions(+), 2 deletions(-) create mode 100644 app/components/BookmarkButton.vue create mode 100644 app/components/HeaderBookmarksDropdown.vue create mode 100644 app/composables/useBookmarks.ts diff --git a/app/components/AppHeader.vue b/app/components/AppHeader.vue index 7d30b29f..4174dbc2 100644 --- a/app/components/AppHeader.vue +++ b/app/components/AppHeader.vue @@ -118,8 +118,12 @@ onKeyStroke(',', e => { - +
+ + + + +const props = defineProps<{ + packageName: string +}>() + +const { useIsBookmarked, toggleBookmark } = useBookmarks() + +const isBookmarked = useIsBookmarked(() => props.packageName) + +function handleClick() { + toggleBookmark(props.packageName) +} + + + diff --git a/app/components/HeaderBookmarksDropdown.vue b/app/components/HeaderBookmarksDropdown.vue new file mode 100644 index 00000000..060ae65a --- /dev/null +++ b/app/components/HeaderBookmarksDropdown.vue @@ -0,0 +1,112 @@ + + + diff --git a/app/composables/useBookmarks.ts b/app/composables/useBookmarks.ts new file mode 100644 index 00000000..71b2797e --- /dev/null +++ b/app/composables/useBookmarks.ts @@ -0,0 +1,92 @@ +import type { RemovableRef } from '@vueuse/core' +import { useLocalStorage } from '@vueuse/core' + +/** + * Bookmark entry with package name and timestamp + */ +export interface Bookmark { + packageName: string + addedAt: number +} + +const STORAGE_KEY = 'npmx-bookmarks' + +// Shared bookmarks instance (singleton per app) +let bookmarksRef: RemovableRef | null = null + +/** + * Composable for managing package bookmarks with localStorage persistence. + * Bookmarks are shared across all components that use this composable. + */ +export function useBookmarks() { + if (!bookmarksRef) { + bookmarksRef = useLocalStorage(STORAGE_KEY, [], { + mergeDefaults: true, + }) + } + + const bookmarks = bookmarksRef + + /** + * Check if a package is bookmarked + */ + function isBookmarked(packageName: string): boolean { + return bookmarks.value.some(b => b.packageName === packageName) + } + + /** + * Reactive computed to check if a specific package is bookmarked + */ + function useIsBookmarked(packageName: MaybeRefOrGetter) { + return computed(() => isBookmarked(toValue(packageName))) + } + + /** + * Add a package to bookmarks + */ + function addBookmark(packageName: string): void { + if (!isBookmarked(packageName)) { + bookmarks.value = [{ packageName, addedAt: Date.now() }, ...bookmarks.value] + } + } + + /** + * Remove a package from bookmarks + */ + function removeBookmark(packageName: string): void { + bookmarks.value = bookmarks.value.filter(b => b.packageName !== packageName) + } + + /** + * Toggle bookmark status for a package + */ + function toggleBookmark(packageName: string): void { + if (isBookmarked(packageName)) { + removeBookmark(packageName) + } else { + addBookmark(packageName) + } + } + + /** + * Clear all bookmarks + */ + function clearBookmarks(): void { + bookmarks.value = [] + } + + const bookmarkCount = computed(() => bookmarks.value.length) + const hasBookmarks = computed(() => bookmarks.value.length > 0) + + return { + bookmarks, + bookmarkCount, + hasBookmarks, + isBookmarked, + useIsBookmarked, + addBookmark, + removeBookmark, + toggleBookmark, + clearBookmarks, + } +} diff --git a/app/pages/[...package].vue b/app/pages/[...package].vue index 350f5482..46302d1e 100644 --- a/app/pages/[...package].vue +++ b/app/pages/[...package].vue @@ -558,12 +558,15 @@ defineOgImageComponent('Package', { - +