feat: add FavoritesService with Directus sync, union merge on login, localStorage migration
This commit is contained in:
180
js/services/favorites.js
Normal file
180
js/services/favorites.js
Normal file
@@ -0,0 +1,180 @@
|
||||
import { directus } from './directus.js'
|
||||
import { auth } from './auth.js'
|
||||
|
||||
const ANON_KEY = 'dgray_favorites'
|
||||
|
||||
class FavoritesService {
|
||||
constructor() {
|
||||
this.ids = new Set()
|
||||
this.loaded = false
|
||||
this.syncing = false
|
||||
this.listeners = []
|
||||
}
|
||||
|
||||
init() {
|
||||
// Migrate old localStorage key
|
||||
const oldFavs = localStorage.getItem('favorites')
|
||||
if (oldFavs && !localStorage.getItem(ANON_KEY)) {
|
||||
localStorage.setItem(ANON_KEY, oldFavs)
|
||||
localStorage.removeItem('favorites')
|
||||
}
|
||||
|
||||
this.ids = new Set(this.getAnonIds())
|
||||
this.loaded = true
|
||||
|
||||
auth.subscribe((loggedIn) => {
|
||||
if (loggedIn) {
|
||||
this.mergeOnLogin()
|
||||
} else {
|
||||
this.saveAnonIds()
|
||||
this.loaded = true
|
||||
this.notify()
|
||||
}
|
||||
})
|
||||
|
||||
if (auth.isLoggedIn()) {
|
||||
this.mergeOnLogin()
|
||||
}
|
||||
}
|
||||
|
||||
subscribe(cb) {
|
||||
this.listeners.push(cb)
|
||||
return () => {
|
||||
this.listeners = this.listeners.filter(l => l !== cb)
|
||||
}
|
||||
}
|
||||
|
||||
notify() {
|
||||
this.listeners.forEach(cb => cb(this.ids))
|
||||
window.dispatchEvent(new CustomEvent('favorites-changed'))
|
||||
}
|
||||
|
||||
// localStorage helpers
|
||||
getAnonIds() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(ANON_KEY) || '[]')
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
saveAnonIds() {
|
||||
localStorage.setItem(ANON_KEY, JSON.stringify([...this.ids]))
|
||||
}
|
||||
|
||||
// Public API
|
||||
isFavorite(id) {
|
||||
return this.ids.has(id)
|
||||
}
|
||||
|
||||
getAll() {
|
||||
return [...this.ids]
|
||||
}
|
||||
|
||||
async toggle(id) {
|
||||
const wasFavorite = this.ids.has(id)
|
||||
|
||||
// Optimistic update
|
||||
if (wasFavorite) {
|
||||
this.ids.delete(id)
|
||||
} else {
|
||||
this.ids.add(id)
|
||||
}
|
||||
this.notify()
|
||||
|
||||
if (auth.isLoggedIn()) {
|
||||
try {
|
||||
if (wasFavorite) {
|
||||
await this.removeRemote(id)
|
||||
} else {
|
||||
await this.addRemote(id)
|
||||
}
|
||||
} catch (e) {
|
||||
// Revert on failure
|
||||
if (wasFavorite) {
|
||||
this.ids.add(id)
|
||||
} else {
|
||||
this.ids.delete(id)
|
||||
}
|
||||
this.notify()
|
||||
console.error('Failed to sync favorite:', e)
|
||||
}
|
||||
} else {
|
||||
this.saveAnonIds()
|
||||
}
|
||||
}
|
||||
|
||||
// Directus API
|
||||
async addRemote(listingId) {
|
||||
const user = await auth.getUser()
|
||||
if (!user) return
|
||||
await directus.post('/items/favorites', {
|
||||
user: user.id,
|
||||
listing: listingId
|
||||
})
|
||||
}
|
||||
|
||||
async removeRemote(listingId) {
|
||||
const user = await auth.getUser()
|
||||
if (!user) return
|
||||
const response = await directus.get('/items/favorites', {
|
||||
filter: {
|
||||
user: { _eq: user.id },
|
||||
listing: { _eq: listingId }
|
||||
},
|
||||
fields: ['id'],
|
||||
limit: 1
|
||||
})
|
||||
const item = response.data?.[0]
|
||||
if (item) {
|
||||
await directus.delete(`/items/favorites/${item.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
async fetchRemote() {
|
||||
const user = await auth.getUser()
|
||||
if (!user) return []
|
||||
const response = await directus.get('/items/favorites', {
|
||||
filter: {
|
||||
user: { _eq: user.id }
|
||||
},
|
||||
fields: ['listing'],
|
||||
limit: 200
|
||||
})
|
||||
return (response.data || []).map(f => f.listing).filter(Boolean)
|
||||
}
|
||||
|
||||
async mergeOnLogin() {
|
||||
this.syncing = true
|
||||
try {
|
||||
const remoteIds = await this.fetchRemote()
|
||||
const localIds = this.getAnonIds()
|
||||
|
||||
// Union merge
|
||||
const merged = new Set([...remoteIds, ...localIds])
|
||||
this.ids = merged
|
||||
|
||||
// Push local-only favorites to server
|
||||
const remoteSet = new Set(remoteIds)
|
||||
const toAdd = localIds.filter(id => !remoteSet.has(id))
|
||||
for (const id of toAdd) {
|
||||
try {
|
||||
await this.addRemote(id)
|
||||
} catch (e) {
|
||||
console.error('Failed to sync favorite to server:', e)
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.removeItem(ANON_KEY)
|
||||
this.loaded = true
|
||||
this.notify()
|
||||
} catch (e) {
|
||||
console.error('Failed to merge favorites:', e)
|
||||
this.loaded = true
|
||||
}
|
||||
this.syncing = false
|
||||
}
|
||||
}
|
||||
|
||||
export const favoritesService = new FavoritesService()
|
||||
export default favoritesService
|
||||
Reference in New Issue
Block a user