import {
Component,
OnInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router, RouterLink, ActivatedRoute } from '@angular/router';
import { Subject, debounceTime, distinctUntilChanged, share, timer } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { ImageRecord, ImageService } from '../services/image.service';
import { TagService } from '../services/tag.service';
const PLACEHOLDER_SVG = `data:image/svg+xml,`;
@Component({
selector: 'app-library',
standalone: true,
imports: [CommonModule, RouterLink],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
Failed to load images. Please check your connection.
{{ tag }}
`,
styles: [`
.library { max-width: 1200px; margin: 0 auto; padding: 24px 16px; }
header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.header-actions { display: flex; align-items: center; gap: 12px; }
.tags-link { color: var(--text-muted); text-decoration: none; font-size: 0.9rem; transition: color var(--transition); }
.tags-link:hover { color: var(--text); }
.upload-btn { padding: 8px 20px; background: var(--accent); color: var(--accent-text); border: none; border-radius: var(--radius); cursor: pointer; font-weight: 600; }
.filter-bar { position: relative; margin-bottom: 24px; }
.filter-bar input { width: 100%; padding: 10px; background: var(--surface); border: 1px solid var(--border); color: var(--text); border-radius: var(--radius); }
.filter-bar input:focus { outline: none; border-color: var(--border-focus); }
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.chip { background: var(--surface-raised); padding: 3px 10px; border-radius: var(--radius-chip); font-size: 0.85rem; display: flex; align-items: center; gap: 4px; }
.chip.small { font-size: 0.75rem; padding: 2px 8px; }
.chip button { background: none; border: none; color: var(--text-muted); cursor: pointer; padding: 0; font-size: 1rem; }
.suggestions { position: absolute; z-index: 10; background: var(--surface); border: 1px solid var(--border); list-style: none; width: 100%; max-height: 200px; overflow-y: auto; border-radius: 0 0 var(--radius) var(--radius); }
.suggestions li { padding: 8px 12px; cursor: pointer; }
.suggestions li:hover { background: var(--surface-raised); }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px; }
.image-card { cursor: pointer; background: var(--surface); border-radius: var(--radius); overflow: hidden; transition: transform var(--transition), box-shadow var(--transition); }
.image-card:hover { transform: translateY(-2px); box-shadow: 0 4px 16px rgba(0,0,0,0.4); }
.image-card img { width: 100%; height: 160px; object-fit: cover; display: block; }
.card-skeleton { height: 200px; }
.tag-row { padding: 6px; display: flex; flex-wrap: wrap; gap: 4px; }
.empty-state { text-align: center; padding: 60px 0; color: var(--text-muted); }
.empty-icon { display: block; font-size: 2rem; margin-bottom: 12px; }
.upload-link { display: inline-block; margin-top: 16px; color: var(--accent); text-decoration: none; font-weight: 600; }
.upload-link:hover { text-decoration: underline; }
.error-card { text-align: center; padding: 40px; background: var(--surface); border-radius: var(--radius); border: 1px solid var(--border); }
.error-card p { color: var(--text-muted); margin-bottom: 16px; }
.retry-btn { padding: 8px 24px; background: var(--surface-raised); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); cursor: pointer; transition: border-color var(--transition); }
.retry-btn:hover { border-color: var(--border-focus); }
.load-more { display: block; margin: 24px auto; padding: 10px 32px; background: var(--surface-raised); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); cursor: pointer; }
`],
})
export class LibraryComponent implements OnInit {
images: ImageRecord[] = [];
activeFilters: string[] = [];
tagSearch = '';
suggestions: { name: string; image_count: number }[] = [];
showSpinner = false;
error = false;
hasMore = false;
readonly skeletonItems = Array(8).fill(null);
private offset = 0;
private readonly limit = 50;
private readonly filterChange$ = new Subject();
constructor(
public imageService: ImageService,
private tagService: TagService,
public router: Router,
private cdr: ChangeDetectorRef,
private route: ActivatedRoute,
) {}
ngOnInit(): void {
const tagsParam = this.route.snapshot.queryParamMap.get('tags');
if (tagsParam) {
this.activeFilters = tagsParam.split(',').map((t) => t.trim()).filter((t) => t.length > 0);
}
this.load();
this.filterChange$.pipe(debounceTime(300), distinctUntilChanged()).subscribe((q) => {
if (q) {
this.tagService.list(q, 10).subscribe((r) => {
this.suggestions = r.items;
this.cdr.markForCheck();
});
} else {
this.suggestions = [];
this.cdr.markForCheck();
}
});
}
load(): void {
this.error = false;
const req$ = this.imageService.list(this.activeFilters, this.limit, this.offset).pipe(share());
timer(150).pipe(takeUntil(req$)).subscribe(() => {
this.showSpinner = true;
this.cdr.markForCheck();
});
req$.subscribe({
next: (res) => {
this.images = [...this.images, ...res.items];
this.offset += res.items.length;
this.hasMore = this.offset < res.total;
this.showSpinner = false;
this.cdr.markForCheck();
},
error: () => {
this.showSpinner = false;
this.error = true;
this.cdr.markForCheck();
},
});
}
onTagInput(event: Event): void {
const val = (event.target as HTMLInputElement).value;
this.tagSearch = val;
this.filterChange$.next(val);
}
addFilter(tag: string): void {
if (!this.activeFilters.includes(tag)) {
this.activeFilters = [...this.activeFilters, tag];
}
this.tagSearch = '';
this.suggestions = [];
this.applyFilter(this.activeFilters);
}
removeFilter(tag: string): void {
this.activeFilters = this.activeFilters.filter((t) => t !== tag);
this.applyFilter(this.activeFilters);
}
applyFilter(tags: string[]): void {
this.activeFilters = tags;
this.offset = 0;
this.images = [];
this.load();
}
loadMore(): void {
this.load();
}
onImgError(event: Event): void {
const img = event.target as HTMLImageElement;
if (!img.src.startsWith('data:')) {
img.src = PLACEHOLDER_SVG;
}
}
}