Files
archy/packages/app/src/stores/favorites.ts
T
DorianandClaude Opus 4.6 258a03d7f1 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>
2026-03-03 20:49:57 +00:00

126 lines
3.3 KiB
TypeScript

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export type FavoriteType = 'film' | 'song' | 'podcast' | 'book' | 'tv' | 'place' | 'article'
export interface FavoriteItem {
id: string
type: FavoriteType
title: string
subtitle?: string
data: unknown
savedAt: number
}
const IDB_STORE = 'favorites'
const DB_NAME = 'aiui-favorites'
const DB_VERSION = 1
function openDB(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const req = indexedDB.open(DB_NAME, DB_VERSION)
req.onupgradeneeded = () => {
const db = req.result
if (!db.objectStoreNames.contains(IDB_STORE)) {
const store = db.createObjectStore(IDB_STORE, { keyPath: 'id' })
store.createIndex('type', 'type', { unique: false })
store.createIndex('savedAt', 'savedAt', { unique: false })
}
}
req.onsuccess = () => resolve(req.result)
req.onerror = () => reject(req.error)
})
}
async function idbGetAll(): Promise<FavoriteItem[]> {
const db = await openDB()
return new Promise((resolve, reject) => {
const tx = db.transaction(IDB_STORE, 'readonly')
const store = tx.objectStore(IDB_STORE)
const req = store.getAll()
req.onsuccess = () => resolve(req.result)
req.onerror = () => reject(req.error)
})
}
async function idbPut(item: FavoriteItem): Promise<void> {
const db = await openDB()
return new Promise((resolve, reject) => {
const tx = db.transaction(IDB_STORE, 'readwrite')
const store = tx.objectStore(IDB_STORE)
const req = store.put(item)
req.onsuccess = () => resolve()
req.onerror = () => reject(req.error)
})
}
async function idbDelete(id: string): Promise<void> {
const db = await openDB()
return new Promise((resolve, reject) => {
const tx = db.transaction(IDB_STORE, 'readwrite')
const store = tx.objectStore(IDB_STORE)
const req = store.delete(id)
req.onsuccess = () => resolve()
req.onerror = () => reject(req.error)
})
}
export const useFavoritesStore = defineStore('favorites', () => {
const items = ref<FavoriteItem[]>([])
const loaded = ref(false)
async function loadFavorites() {
try {
items.value = await idbGetAll()
} catch {
// IDB unavailable — in-memory only
}
loaded.value = true
}
loadFavorites()
const sortedItems = computed(() =>
[...items.value].sort((a, b) => b.savedAt - a.savedAt)
)
function isFavorited(id: string): boolean {
return items.value.some(i => i.id === id)
}
async function addFavorite(item: Omit<FavoriteItem, 'savedAt'>) {
if (isFavorited(item.id)) return
const fav: FavoriteItem = { ...item, savedAt: Date.now() }
items.value = [...items.value, fav]
idbPut(fav).catch(() => {})
}
async function removeFavorite(id: string) {
items.value = items.value.filter(i => i.id !== id)
idbDelete(id).catch(() => {})
}
async function toggleFavorite(item: Omit<FavoriteItem, 'savedAt'>) {
if (isFavorited(item.id)) {
await removeFavorite(item.id)
} else {
await addFavorite(item)
}
}
function getFavoritesByType(type: FavoriteType): FavoriteItem[] {
return sortedItems.value.filter(i => i.type === type)
}
return {
items,
sortedItems,
loaded,
isFavorited,
addFavorite,
removeFavorite,
toggleFavorite,
getFavoritesByType,
}
})