generated from nhcarrigan/template
460 lines
12 KiB
TypeScript
460 lines
12 KiB
TypeScript
/**
|
|
* @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: `
|
|
<div class="container">
|
|
<div class="header-section">
|
|
<h2>My Likes</h2>
|
|
<p class="subtitle">All the items you've liked across the library</p>
|
|
</div>
|
|
|
|
@if (!authService.isAuthenticated()) {
|
|
<div class="not-authenticated">
|
|
<p>Please log in to view your liked items.</p>
|
|
</div>
|
|
} @else if (loading()) {
|
|
<div class="loading">Loading your liked items...</div>
|
|
} @else if (likedItems().length === 0) {
|
|
<div class="empty-state">
|
|
<p>You haven't liked any items yet!</p>
|
|
<p class="hint">Explore the library and click the heart button on items you enjoy.</p>
|
|
</div>
|
|
} @else {
|
|
<div class="filters">
|
|
<button
|
|
(click)="setFilter('all')"
|
|
[class.active]="typeFilter() === 'all'"
|
|
class="filter-btn"
|
|
>
|
|
All ({{ likedItems().length }})
|
|
</button>
|
|
<button
|
|
(click)="setFilter('book')"
|
|
[class.active]="typeFilter() === 'book'"
|
|
class="filter-btn"
|
|
>
|
|
Books ({{ bookCount() }})
|
|
</button>
|
|
<button
|
|
(click)="setFilter('game')"
|
|
[class.active]="typeFilter() === 'game'"
|
|
class="filter-btn"
|
|
>
|
|
Games ({{ gameCount() }})
|
|
</button>
|
|
<button
|
|
(click)="setFilter('show')"
|
|
[class.active]="typeFilter() === 'show'"
|
|
class="filter-btn"
|
|
>
|
|
Shows ({{ showCount() }})
|
|
</button>
|
|
<button
|
|
(click)="setFilter('manga')"
|
|
[class.active]="typeFilter() === 'manga'"
|
|
class="filter-btn"
|
|
>
|
|
Manga ({{ mangaCount() }})
|
|
</button>
|
|
<button
|
|
(click)="setFilter('music')"
|
|
[class.active]="typeFilter() === 'music'"
|
|
class="filter-btn"
|
|
>
|
|
Music ({{ musicCount() }})
|
|
</button>
|
|
<button
|
|
(click)="setFilter('art')"
|
|
[class.active]="typeFilter() === 'art'"
|
|
class="filter-btn"
|
|
>
|
|
Art ({{ artCount() }})
|
|
</button>
|
|
</div>
|
|
|
|
<app-pagination
|
|
[currentPage]="currentPage()"
|
|
[pageSize]="pageSize()"
|
|
[totalItems]="totalFilteredItems()"
|
|
(pageChange)="onPageChange($event)"
|
|
(pageSizeChange)="onPageSizeChange($event)"
|
|
></app-pagination>
|
|
|
|
<div class="liked-items-grid">
|
|
@for (likedItem of paginatedItems(); track likedItem.like.id) {
|
|
<div class="liked-item-card">
|
|
<div class="item-type-badge">{{ getTypeBadgeLabel(likedItem.like.entityType) }}</div>
|
|
|
|
@if (getItemImage(likedItem)) {
|
|
<img
|
|
[src]="getItemImage(likedItem)"
|
|
[alt]="getItemTitle(likedItem)"
|
|
class="item-image"
|
|
>
|
|
} @else {
|
|
<div class="item-image placeholder">{{ getTypePlaceholderEmoji(likedItem.like.entityType) }}</div>
|
|
}
|
|
|
|
<div class="item-info">
|
|
<h3>{{ getItemTitle(likedItem) }}</h3>
|
|
<p class="item-subtitle">{{ getItemSubtitle(likedItem) }}</p>
|
|
<p class="liked-date">Liked: {{ formatDate(likedItem.like.createdAt) }}</p>
|
|
|
|
<app-like-button
|
|
[entityType]="likedItem.like.entityType"
|
|
[entityId]="likedItem.like.entityId"
|
|
></app-like-button>
|
|
|
|
<a
|
|
[routerLink]="getItemLink(likedItem)"
|
|
class="view-link"
|
|
>
|
|
View Details →
|
|
</a>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<app-pagination
|
|
[currentPage]="currentPage()"
|
|
[pageSize]="pageSize()"
|
|
[totalItems]="totalFilteredItems()"
|
|
(pageChange)="onPageChange($event)"
|
|
(pageSizeChange)="onPageSizeChange($event)"
|
|
></app-pagination>
|
|
}
|
|
</div>
|
|
`,
|
|
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<LikedItemDto[]>([]);
|
|
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'
|
|
});
|
|
}
|
|
} |