generated from nhcarrigan/template
feat: add ability to like books
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* @copyright 2026 NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import { Injectable, inject, signal, computed } from '@angular/core';
|
||||
import { Observable, map, tap, catchError, of } from 'rxjs';
|
||||
import { ApiService } from './api.service';
|
||||
import { CreateLikeDto, LikeResponse, LikedItemDto, Like } from '@library/shared-types';
|
||||
|
||||
interface LikeState {
|
||||
entityType: Like['entityType'];
|
||||
entityId: string;
|
||||
liked: boolean;
|
||||
count: number;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class LikesService {
|
||||
private api = inject(ApiService);
|
||||
|
||||
// Store like states for caching
|
||||
private likeStates = signal<Map<string, LikeState>>(new Map());
|
||||
|
||||
/**
|
||||
* Toggle like on an item.
|
||||
*/
|
||||
toggleLike(entityType: Like['entityType'], entityId: string): Observable<LikeResponse> {
|
||||
const dto: CreateLikeDto = { entityType, entityId };
|
||||
|
||||
return this.api.post<LikeResponse>('/likes/toggle', dto).pipe(
|
||||
tap(response => {
|
||||
const key = this.getCacheKey(entityType, entityId);
|
||||
this.likeStates.update(states => {
|
||||
const newStates = new Map(states);
|
||||
newStates.set(key, {
|
||||
entityType,
|
||||
entityId,
|
||||
liked: response.liked,
|
||||
count: response.count
|
||||
});
|
||||
return newStates;
|
||||
});
|
||||
}),
|
||||
catchError(() => {
|
||||
// On error, return current state or default
|
||||
const key = this.getCacheKey(entityType, entityId);
|
||||
const currentState = this.likeStates().get(key);
|
||||
return of({
|
||||
liked: currentState?.liked || false,
|
||||
count: currentState?.count || 0
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get like count for an item.
|
||||
*/
|
||||
getLikeCount(entityType: Like['entityType'], entityId: string): Observable<number> {
|
||||
return this.api.get<{ count: number }>(`/likes/count?entityType=${entityType}&entityId=${entityId}`).pipe(
|
||||
map(response => response.count),
|
||||
tap(count => {
|
||||
const key = this.getCacheKey(entityType, entityId);
|
||||
this.likeStates.update(states => {
|
||||
const newStates = new Map(states);
|
||||
const currentState = newStates.get(key);
|
||||
newStates.set(key, {
|
||||
entityType,
|
||||
entityId,
|
||||
liked: currentState?.liked || false,
|
||||
count
|
||||
});
|
||||
return newStates;
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current user has liked an item.
|
||||
*/
|
||||
getUserLikeStatus(entityType: Like['entityType'], entityId: string): Observable<boolean> {
|
||||
return this.api.get<{ liked: boolean }>(`/likes/status?entityType=${entityType}&entityId=${entityId}`).pipe(
|
||||
map(response => response.liked),
|
||||
tap(liked => {
|
||||
const key = this.getCacheKey(entityType, entityId);
|
||||
this.likeStates.update(states => {
|
||||
const newStates = new Map(states);
|
||||
const currentState = newStates.get(key);
|
||||
newStates.set(key, {
|
||||
entityType,
|
||||
entityId,
|
||||
liked,
|
||||
count: currentState?.count || 0
|
||||
});
|
||||
return newStates;
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all items liked by the current user.
|
||||
*/
|
||||
getUserLikedItems(entityType?: Like['entityType']): Observable<LikedItemDto[]> {
|
||||
const query = entityType ? `?entityType=${entityType}` : '';
|
||||
return this.api.get<LikedItemDto[]>(`/likes/user${query}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bulk like statuses for multiple items.
|
||||
*/
|
||||
getBulkLikeStatuses(items: Array<{ entityType: string; entityId: string }>): Observable<Array<{
|
||||
entityType: string;
|
||||
entityId: string;
|
||||
liked: boolean;
|
||||
count: number;
|
||||
}>> {
|
||||
return this.api.post<Array<{
|
||||
entityType: string;
|
||||
entityId: string;
|
||||
liked: boolean;
|
||||
count: number;
|
||||
}>>('/likes/bulk-status', { items }).pipe(
|
||||
tap(results => {
|
||||
this.likeStates.update(states => {
|
||||
const newStates = new Map(states);
|
||||
results.forEach(result => {
|
||||
const key = this.getCacheKey(result.entityType as Like['entityType'], result.entityId);
|
||||
newStates.set(key, {
|
||||
entityType: result.entityType as Like['entityType'],
|
||||
entityId: result.entityId,
|
||||
liked: result.liked,
|
||||
count: result.count
|
||||
});
|
||||
});
|
||||
return newStates;
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get like state from cache.
|
||||
*/
|
||||
getCachedLikeState(entityType: Like['entityType'], entityId: string): LikeState | undefined {
|
||||
const key = this.getCacheKey(entityType, entityId);
|
||||
return this.likeStates().get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create computed signal for specific item's like state.
|
||||
*/
|
||||
createLikeStateSignal(entityType: Like['entityType'], entityId: string) {
|
||||
const key = this.getCacheKey(entityType, entityId);
|
||||
return computed(() => {
|
||||
const state = this.likeStates().get(key);
|
||||
return {
|
||||
liked: state?.liked || false,
|
||||
count: state?.count || 0
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private getCacheKey(entityType: Like['entityType'], entityId: string): string {
|
||||
return `${entityType}:${entityId}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user