/** * @copyright 2026 NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan */ import { Component, OnInit, inject, signal, computed } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterLink } from '@angular/router'; import { LikesService } from '../../services/likes.service'; import { AuthService } from '../../services/auth.service'; import { PaginationComponent } from '../shared/pagination.component'; import { LikeButtonComponent } from '../shared/like-button.component'; import { LikedItemDto, Like } from '@library/shared-types'; @Component({ selector: 'app-my-likes', standalone: true, imports: [CommonModule, RouterLink, PaginationComponent, LikeButtonComponent], template: `

My Likes

All the items you've liked across the library

@if (!authService.isAuthenticated()) {

Please log in to view your liked items.

} @else if (loading()) {
Loading your liked items...
} @else if (likedItems().length === 0) {

You haven't liked any items yet!

Explore the library and click the heart button on items you enjoy.

} @else {
@for (likedItem of paginatedItems(); track likedItem.like.id) {
{{ getTypeBadgeLabel(likedItem.like.entityType) }}
@if (getItemImage(likedItem)) { } @else {
{{ getTypePlaceholderEmoji(likedItem.like.entityType) }}
}

{{ getItemTitle(likedItem) }}

{{ getItemSubtitle(likedItem) }}

Liked: {{ formatDate(likedItem.like.createdAt) }}

View Details →
}
}
`, styles: [` .container { max-width: 1200px; margin: 0 auto; padding: 2rem; } .header-section { margin-bottom: 2rem; } .header-section h2 { color: var(--witch-purple); font-size: 2.5rem; margin-bottom: 0.5rem; } .subtitle { color: var(--witch-plum); font-size: 1.1rem; margin: 0; } .not-authenticated, .loading, .empty-state { background: var(--witch-lavender); padding: 3rem; text-align: center; border-radius: 12px; margin: 2rem 0; } .empty-state .hint { color: var(--witch-plum); margin-top: 1rem; } .filters { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 2rem; padding-bottom: 1rem; border-bottom: 2px solid var(--witch-plum); } .filter-btn { padding: 0.5rem 1rem; border: 2px solid var(--witch-plum); background: transparent; color: var(--witch-purple); border-radius: 20px; cursor: pointer; transition: all 0.3s ease; } .filter-btn:hover { background: var(--witch-lavender); } .filter-btn.active { background: var(--witch-rose); color: white; border-color: var(--witch-rose); } .liked-items-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 2rem; margin: 2rem 0; } .liked-item-card { background: white; border: 2px solid var(--witch-plum); border-radius: 12px; overflow: hidden; transition: all 0.3s ease; position: relative; } .liked-item-card:hover { transform: translateY(-4px); box-shadow: 0 8px 16px var(--witch-shadow); } .item-type-badge { position: absolute; top: 10px; right: 10px; background: var(--witch-rose); color: white; padding: 0.25rem 0.75rem; border-radius: 20px; font-size: 0.85rem; text-transform: capitalize; z-index: 1; } .item-image { width: 100%; height: 200px; object-fit: cover; } .item-image.placeholder { display: flex; align-items: center; justify-content: center; background: var(--witch-lavender); font-size: 4rem; } .item-info { padding: 1.5rem; } .item-info h3 { color: var(--witch-purple); margin-bottom: 0.5rem; font-size: 1.3rem; } .item-subtitle { color: var(--witch-plum); margin-bottom: 0.5rem; } .liked-date { color: var(--witch-silver); font-size: 0.9rem; margin-bottom: 1rem; } .view-link { display: inline-block; margin-top: 1rem; color: var(--witch-rose); text-decoration: none; font-weight: 600; transition: all 0.3s ease; } .view-link:hover { color: var(--witch-plum); transform: translateX(4px); } @media (max-width: 768px) { .liked-items-grid { grid-template-columns: 1fr; } } `] }) export class MyLikesComponent implements OnInit { private likesService = inject(LikesService); authService = inject(AuthService); loading = signal(false); likedItems = signal([]); typeFilter = signal<'all' | Like['entityType']>('all'); currentPage = signal(1); pageSize = signal(12); // Computed signals for filtering and pagination filteredItems = computed(() => { const filter = this.typeFilter(); const items = this.likedItems(); if (filter === 'all') { return items; } return items.filter(item => item.like.entityType === filter); }); totalFilteredItems = computed(() => this.filteredItems().length); paginatedItems = computed(() => { const items = this.filteredItems(); const page = this.currentPage(); const size = this.pageSize(); const start = (page - 1) * size; const end = start + size; return items.slice(start, end); }); // Computed counts for each type bookCount = computed(() => this.likedItems().filter(item => item.like.entityType === 'book').length); gameCount = computed(() => this.likedItems().filter(item => item.like.entityType === 'game').length); showCount = computed(() => this.likedItems().filter(item => item.like.entityType === 'show').length); mangaCount = computed(() => this.likedItems().filter(item => item.like.entityType === 'manga').length); musicCount = computed(() => this.likedItems().filter(item => item.like.entityType === 'music').length); artCount = computed(() => this.likedItems().filter(item => item.like.entityType === 'art').length); ngOnInit() { if (this.authService.isAuthenticated()) { this.loadLikedItems(); } } loadLikedItems() { this.loading.set(true); this.likesService.getUserLikedItems().subscribe({ next: (items) => { this.likedItems.set(items); this.loading.set(false); }, error: () => { this.loading.set(false); } }); } setFilter(filter: 'all' | Like['entityType']) { this.typeFilter.set(filter); this.currentPage.set(1); // Reset to first page when filtering } onPageChange(page: number) { this.currentPage.set(page); } onPageSizeChange(size: number) { this.pageSize.set(size); this.currentPage.set(1); // Reset to first page when changing page size } getItemTitle(likedItem: LikedItemDto): string { const item = likedItem.item; return item.title || item.name || 'Untitled'; } getItemSubtitle(likedItem: LikedItemDto): string { const item = likedItem.item; const type = likedItem.like.entityType; switch (type) { case 'book': return `by ${item.author || 'Unknown author'}`; case 'game': return item.platform || 'Platform not specified'; case 'show': return item.type === 'movie' ? 'Movie' : 'TV Show'; case 'manga': return item.volumeCount ? `${item.volumeCount} volumes` : 'Ongoing'; case 'music': return `${item.artist || 'Unknown artist'} - ${item.type || 'Album'}`; case 'art': return `by ${item.artist || 'Unknown artist'}`; default: return ''; } } getItemImage(likedItem: LikedItemDto): string | null { const item = likedItem.item; return item.coverImage || item.imageUrl || null; } getItemLink(likedItem: LikedItemDto): string[] { const type = likedItem.like.entityType; const id = likedItem.like.entityId; switch (type) { case 'book': return ['/books']; case 'game': return ['/games']; case 'show': return ['/shows']; case 'manga': return ['/manga']; case 'music': return ['/music']; case 'art': return ['/art']; default: return ['/']; } } getTypeBadgeLabel(type: Like['entityType']): string { return type.charAt(0).toUpperCase() + type.slice(1); } getTypePlaceholderEmoji(type: Like['entityType']): string { switch (type) { case 'book': return '📚'; case 'game': return '🎮'; case 'show': return '🎬'; case 'manga': return '📖'; case 'music': return '🎵'; case 'art': return '🎨'; default: return '❤️'; } } formatDate(date: Date | string): string { const d = new Date(date); return d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' }); } }