/**
* @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: `
@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'
});
}
}