generated from nhcarrigan/template
172 lines
5.0 KiB
TypeScript
172 lines
5.0 KiB
TypeScript
/**
|
|
* @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}`;
|
|
}
|
|
} |