generated from nhcarrigan/template
feat: add ability to like books
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* @copyright 2026 NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import { Component, Input, inject, OnInit, signal, effect } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { LikesService } from '../../services/likes.service';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { Like } from '@library/shared-types';
|
||||
import { take } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-like-button',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
template: `
|
||||
<div class="like-container">
|
||||
<button
|
||||
type="button"
|
||||
class="like-button"
|
||||
[class.liked]="liked()"
|
||||
[disabled]="loading() || !isAuthenticated()"
|
||||
(click)="toggleLike()"
|
||||
[title]="getTitle()"
|
||||
>
|
||||
<span class="heart-icon">{{ liked() ? '❤️' : '🤍' }}</span>
|
||||
<span class="like-count">{{ count() }}</span>
|
||||
</button>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
.like-container {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.like-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 9999px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.like-button:hover:not(:disabled) {
|
||||
background: var(--hover-background);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.like-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.like-button.liked {
|
||||
border-color: var(--primary-color);
|
||||
background: var(--primary-light-background);
|
||||
}
|
||||
|
||||
.heart-icon {
|
||||
font-size: 1.1rem;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.like-button:hover:not(:disabled) .heart-icon {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.like-button:active:not(:disabled) .heart-icon {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.like-count {
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class LikeButtonComponent implements OnInit {
|
||||
@Input({ required: true }) entityType!: Like['entityType'];
|
||||
@Input({ required: true }) entityId!: string;
|
||||
|
||||
private likesService = inject(LikesService);
|
||||
private authService = inject(AuthService);
|
||||
|
||||
liked = signal(false);
|
||||
count = signal(0);
|
||||
loading = signal(false);
|
||||
isAuthenticated = signal(false);
|
||||
|
||||
ngOnInit() {
|
||||
// Set authentication state
|
||||
this.isAuthenticated.set(this.authService.isAuthenticated());
|
||||
|
||||
// Load initial state
|
||||
this.loadLikeState();
|
||||
}
|
||||
|
||||
private loadLikeState() {
|
||||
// Check cache first
|
||||
const cachedState = this.likesService.getCachedLikeState(this.entityType, this.entityId);
|
||||
if (cachedState) {
|
||||
this.liked.set(cachedState.liked);
|
||||
this.count.set(cachedState.count);
|
||||
}
|
||||
|
||||
// Always get count (public endpoint)
|
||||
this.likesService.getLikeCount(this.entityType, this.entityId)
|
||||
.pipe(take(1))
|
||||
.subscribe(count => {
|
||||
this.count.set(count);
|
||||
});
|
||||
|
||||
// Get user like status if authenticated
|
||||
if (this.isAuthenticated()) {
|
||||
this.likesService.getUserLikeStatus(this.entityType, this.entityId)
|
||||
.pipe(take(1))
|
||||
.subscribe(liked => {
|
||||
this.liked.set(liked);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
toggleLike() {
|
||||
if (!this.isAuthenticated() || this.loading()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading.set(true);
|
||||
|
||||
// Optimistic update
|
||||
const newLiked = !this.liked();
|
||||
const newCount = newLiked ? this.count() + 1 : Math.max(0, this.count() - 1);
|
||||
this.liked.set(newLiked);
|
||||
this.count.set(newCount);
|
||||
|
||||
this.likesService.toggleLike(this.entityType, this.entityId)
|
||||
.pipe(take(1))
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
this.liked.set(response.liked);
|
||||
this.count.set(response.count);
|
||||
this.loading.set(false);
|
||||
},
|
||||
error: () => {
|
||||
// Revert on error
|
||||
this.liked.set(!newLiked);
|
||||
this.count.set(newLiked ? Math.max(0, newCount - 1) : newCount + 1);
|
||||
this.loading.set(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getTitle(): string {
|
||||
if (!this.isAuthenticated()) {
|
||||
return 'Sign in to like';
|
||||
}
|
||||
return this.liked() ? 'Unlike' : 'Like';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user