feat(app): add bookmarks/favorites with IndexedDB persistence
Create favorites Pinia store with IndexedDB backend for persistent favorites. Add FavoriteButton.vue heart toggle (accent-colored when active). Create FavoritesGrid.vue with type filtering. Wire favorite buttons into FilmCard and SongCard. Add Favorites tab to ContentPanel that appears when items are saved. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
1991d75850
commit
258a03d7f1
@@ -33,11 +33,11 @@
|
||||
|
||||
<!-- Tab bar (when multiple tabs available) -->
|
||||
<div
|
||||
v-if="availableTabs.length > 1 && !hasDetailOpen"
|
||||
v-if="displayTabs.length > 1 && !hasDetailOpen"
|
||||
class="flex items-center gap-1 px-3 pt-3 pb-1 shrink-0 overflow-x-auto scrollbar-hide"
|
||||
>
|
||||
<button
|
||||
v-for="tab in availableTabs"
|
||||
v-for="tab in displayTabs"
|
||||
:key="tab"
|
||||
class="text-[10px] px-2.5 py-1.5 rounded-lg font-medium whitespace-nowrap transition-all duration-150"
|
||||
:class="activeTab === tab
|
||||
@@ -45,7 +45,7 @@
|
||||
: isDark
|
||||
? 'text-white/40 hover:text-white/70 hover:bg-white/5'
|
||||
: 'text-gray-500 hover:text-gray-800 hover:bg-black/5'"
|
||||
@click="setActiveTab(tab)"
|
||||
@click="tab === 'favorites' ? (activeTab = tab) : setActiveTab(tab)"
|
||||
>
|
||||
{{ tabLabel(tab) }}
|
||||
</button>
|
||||
@@ -148,6 +148,9 @@
|
||||
<NostrGrid
|
||||
v-else-if="activeTab === 'nostr'"
|
||||
/>
|
||||
<FavoritesGrid
|
||||
v-else-if="activeTab === 'favorites'"
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -170,13 +173,17 @@ import ArticleDetail from './ArticleDetail.vue'
|
||||
import MagazineGrid from './MagazineGrid.vue'
|
||||
import ProjectGrid from './ProjectGrid.vue'
|
||||
import NostrGrid from './NostrGrid.vue'
|
||||
import FavoritesGrid from './FavoritesGrid.vue'
|
||||
import ErrorBoundary from '@/components/ui/ErrorBoundary.vue'
|
||||
import { useFavoritesStore } from '@/stores/favorites'
|
||||
|
||||
// Film and song renderers loaded from plugin registry
|
||||
const filmRenderer = computed(() => getRendererForContentType('film'))
|
||||
const songRenderer = computed(() => getRendererForContentType('song'))
|
||||
|
||||
const { isDark } = useTheme()
|
||||
const favoritesStore = useFavoritesStore()
|
||||
|
||||
const {
|
||||
panelOpen,
|
||||
panelFilms,
|
||||
@@ -221,6 +228,14 @@ const hasDetailOpen = computed(() =>
|
||||
const windowWidth = ref(window.innerWidth)
|
||||
const isMobile = computed(() => windowWidth.value < 1024)
|
||||
|
||||
const displayTabs = computed(() => {
|
||||
const tabs = [...availableTabs.value]
|
||||
if (favoritesStore.items.length > 0 && !tabs.includes('favorites')) {
|
||||
tabs.push('favorites')
|
||||
}
|
||||
return tabs
|
||||
})
|
||||
|
||||
function onResize() {
|
||||
windowWidth.value = window.innerWidth
|
||||
}
|
||||
@@ -239,6 +254,7 @@ const TAB_LABELS: Record<ContentTab, string> = {
|
||||
code: 'Code',
|
||||
'design-system': 'Design',
|
||||
nostr: 'Nostr',
|
||||
favorites: 'Favorites',
|
||||
}
|
||||
|
||||
function tabLabel(tab: ContentTab): string {
|
||||
|
||||
Reference in New Issue
Block a user