feat: DB-backed favorites replace localStorage

- New clip_favorites table (clip_id, user_id DEFAULT 'default', created_at)
- API: POST /clips/:id/favorite (toggle), GET /favorites, GET /clips/:id/favorite
- Frontend: loads favorites on mount from API, toggles via POST
- Removes localStorage dependency - favorites survive cache clears
- Ready for multi-user: just add real user_id when auth arrives
This commit is contained in:
rin.kagamine committed 2026-05-31 11:35:01 -04:00
1 parent ba4d4a95fe
commit b008a6790a
3 files changed
+110 -19

No files matched your search

+25 -17
View File
@@ -98,24 +98,32 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
const [showFavorites, setShowFavorites] = useState(false)
const debouncedSearch = useDebounce(search, 300)
// Favorites
const [favorites, setFavorites] = useState<Set<string>>(() => {
try {
const raw = localStorage.getItem('kithara-favorites')
return raw ? new Set(JSON.parse(raw)) : new Set()
} catch {
return new Set()
}
})
// Favorites (DB-backed)
const [favorites, setFavorites] = useState<Set<string>>(new Set())
const toggleFavorite = useCallback((id: string) => {
setFavorites(prev => {
const next = new Set(prev)
if (next.has(id)) next.delete(id)
else next.add(id)
localStorage.setItem('kithara-favorites', JSON.stringify([...next]))
return next
})
// Load favorites on mount
useEffect(() => {
fetch('/api/favorites')
.then(res => res.json())
.then(data => setFavorites(new Set(data.ids || [])))
.catch(() => {}) // silently fail
}, [])
const toggleFavorite = useCallback(async (id: string) => {
try {
const res = await fetch(`/api/clips/${id}/favorite`, { method: 'POST' })
if (res.ok) {
const data = await res.json()
setFavorites(prev => {
const next = new Set(prev)
if (data.favorited) next.add(id)
else next.delete(id)
return next
})
}
} catch {
// silently fail
}
}, [])
// Pagination + view