From a9764a4a822d7557512f76a58ddaa2219e568489 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Wed, 4 Feb 2026 20:37:51 -0800 Subject: [PATCH] feat: add ability to search --- apps/frontend/project.json | 4 +- .../components/art/art-gallery.component.ts | 188 ++++++++++++- .../components/books/books-list.component.ts | 250 +++++++++++++++++- .../components/games/games-list.component.ts | 185 ++++++++++++- .../components/manga/manga-list.component.ts | 185 ++++++++++++- .../components/music/music-list.component.ts | 175 ++++++++++++ .../components/shows/shows-list.component.ts | 185 ++++++++++++- 7 files changed, 1149 insertions(+), 23 deletions(-) diff --git a/apps/frontend/project.json b/apps/frontend/project.json index 2d72fba..ea4900a 100644 --- a/apps/frontend/project.json +++ b/apps/frontend/project.json @@ -33,8 +33,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" + "maximumWarning": "8kb", + "maximumError": "12kb" } ], "outputHashing": "all", diff --git a/apps/frontend/src/app/components/art/art-gallery.component.ts b/apps/frontend/src/app/components/art/art-gallery.component.ts index 6e05d62..d8d876f 100644 --- a/apps/frontend/src/app/components/art/art-gallery.component.ts +++ b/apps/frontend/src/app/components/art/art-gallery.component.ts @@ -331,11 +331,56 @@ import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity, Link } from } +
+ + + @if (searchQuery() || selectedTags().length > 0) { + + } +
+ + @if (showFilters()) { +
+
+

Filter by Tags

+
+ @for (tag of allTags(); track tag) { + + } + @empty { +

No tags available

+ } +
+
+
+ } + @if (loading()) {
Loading gallery...
- } @else if (artPieces().length === 0) { + } @else if (filteredArtPieces().length === 0) {
-

No artwork in the gallery yet.

+

No artwork found with these filters.

} @else { ([]); + showFilters = signal(false); + // Comments state comments = signal>({}); commentsLoading = signal>({}); @@ -1104,15 +1230,47 @@ export class ArtGalleryComponent implements OnInit { editLinkTitle = ''; editLinkUrl = ''; - // Computed properties for pagination + // Computed properties + allTags = computed(() => { + const tagsSet = new Set(); + this.artPieces().forEach(art => { + art.tags?.forEach(tag => tagsSet.add(tag)); + }); + return Array.from(tagsSet).sort(); + }); + + filteredArtPieces = computed(() => { + let artPieces = this.artPieces(); + + // Apply search filter + const searchQuery = this.searchQuery().toLowerCase().trim(); + if (searchQuery) { + artPieces = artPieces.filter(art => + art.title.toLowerCase().includes(searchQuery) || + art.artist.toLowerCase().includes(searchQuery) || + art.description?.toLowerCase().includes(searchQuery) + ); + } + + // Apply tag filter + const selectedTags = this.selectedTags(); + if (selectedTags.length > 0) { + artPieces = artPieces.filter(art => + selectedTags.every(tag => art.tags?.includes(tag)) + ); + } + + return artPieces; + }); + paginatedArtPieces = computed(() => { - const artPieces = this.artPieces(); + const artPieces = this.filteredArtPieces(); const start = (this.currentPage() - 1) * this.pageSize(); const end = start + this.pageSize(); return artPieces.slice(start, end); }); - totalArtPieces = computed(() => this.artPieces().length); + totalArtPieces = computed(() => this.filteredArtPieces().length); ngOnInit() { this.loadArt(); @@ -1431,4 +1589,24 @@ export class ArtGalleryComponent implements OnInit { const newPage = Math.floor(firstItemIndex / pageSize) + 1; this.currentPage.set(newPage); } + + toggleTag(tag: string) { + const current = this.selectedTags(); + if (current.includes(tag)) { + this.selectedTags.set(current.filter(t => t !== tag)); + } else { + this.selectedTags.set([...current, tag]); + } + this.currentPage.set(1); // Reset to first page when tags change + } + + clearFilters() { + this.searchQuery.set(''); + this.selectedTags.set([]); + this.currentPage.set(1); + } + + toggleFilters() { + this.showFilters.update(v => !v); + } } diff --git a/apps/frontend/src/app/components/books/books-list.component.ts b/apps/frontend/src/app/components/books/books-list.component.ts index 68cc5ea..10d3fd2 100644 --- a/apps/frontend/src/app/components/books/books-list.component.ts +++ b/apps/frontend/src/app/components/books/books-list.component.ts @@ -397,6 +397,54 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti } +
+ + +
+ + @if (showFilters()) { +
+
+ +
+ @for (tag of allTags(); track tag) { + + } + @empty { +

No tags available

+ } +
+ @if (selectedTags().length > 0) { + + } +
+
+ } +
+ @if (searchQuery() || selectedTags().length > 0) { + + } +
+ + @if (showFilters()) { +
+
+

Filter by Tags

+
+ @for (tag of allTags(); track tag) { + + } + @empty { +

No tags available

+ } +
+
+
+ } +
+ @if (searchQuery() || selectedTags().length > 0) { + + } +
+ + @if (showFilters()) { +
+
+

Filter by Tags

+
+ @for (tag of allTags(); track tag) { + + } + @empty { +

No tags available

+ } +
+
+
+ } +
+ @if (searchQuery() || selectedTags().length > 0) { + + } +
+ + @if (showFilters()) { +
+
+

Filter by Tags

+
+ @for (tag of allTags(); track tag) { + + } + @empty { +

No tags available

+ } +
+
+
+ } +
Type: @@ -716,6 +761,82 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment, margin-top: 1rem; } + .search-section { + display: flex; + gap: 1rem; + align-items: center; + margin-bottom: 1rem; + flex-wrap: wrap; + } + + .search-input { + flex: 1; + min-width: 250px; + padding: 0.5rem 1rem; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 1rem; + } + + .search-input:focus { + outline: none; + border-color: #8b5cf6; + } + + .advanced-filters { + background: #f8f9fa; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; + } + + .filter-group h4 { + margin: 0 0 0.75rem 0; + color: #374151; + font-size: 0.95rem; + font-weight: 600; + } + + .tags-filter { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + } + + .tag-checkbox { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; + padding: 0.25rem 0.75rem; + border: 1px solid #e5e7eb; + border-radius: 20px; + background: white; + transition: all 0.2s; + } + + .tag-checkbox:hover { + border-color: #8b5cf6; + background: #f3e8ff; + } + + .tag-checkbox input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; + } + + .tag-checkbox span { + font-size: 0.875rem; + color: #374151; + } + + .no-tags { + color: #6b7280; + font-style: italic; + font-size: 0.875rem; + } + .filters { display: flex; flex-direction: column; @@ -1265,6 +1386,11 @@ export class MusicListComponent implements OnInit { currentPage = signal(1); pageSize = signal(25); + // Search and filter state + searchQuery = signal(''); + selectedTags = signal([]); + showFilters = signal(false); + // Comments state comments = signal>({}); commentsLoading = signal>({}); @@ -1304,6 +1430,14 @@ export class MusicListComponent implements OnInit { completedCount = computed(() => this.music().filter(m => m.status === MusicStatus.completed).length); wantToListenCount = computed(() => this.music().filter(m => m.status === MusicStatus.wantToListen).length); + allTags = computed(() => { + const tagsSet = new Set(); + this.music().forEach(music => { + music.tags?.forEach(tag => tagsSet.add(tag)); + }); + return Array.from(tagsSet).sort(); + }); + filteredMusic = computed(() => { let filtered = this.music(); @@ -1317,6 +1451,25 @@ export class MusicListComponent implements OnInit { filtered = filtered.filter(music => music.status === statusFilter); } + // Apply search filter + const searchQuery = this.searchQuery().toLowerCase().trim(); + if (searchQuery) { + filtered = filtered.filter(music => + music.title.toLowerCase().includes(searchQuery) || + music.artist.toLowerCase().includes(searchQuery) || + music.notes?.toLowerCase().includes(searchQuery) || + this.getTypeLabel(music.type).toLowerCase().includes(searchQuery) + ); + } + + // Apply tag filter + const selectedTags = this.selectedTags(); + if (selectedTags.length > 0) { + filtered = filtered.filter(music => + selectedTags.every(tag => music.tags?.includes(tag)) + ); + } + return filtered; }); @@ -1377,6 +1530,28 @@ export class MusicListComponent implements OnInit { this.currentPage.set(1); // Reset to first page when filter changes } + toggleTag(tag: string) { + const current = this.selectedTags(); + if (current.includes(tag)) { + this.selectedTags.set(current.filter(t => t !== tag)); + } else { + this.selectedTags.set([...current, tag]); + } + this.currentPage.set(1); // Reset to first page when tags change + } + + clearFilters() { + this.searchQuery.set(''); + this.selectedTags.set([]); + this.typeFilter.set('all'); + this.statusFilter.set('all'); + this.currentPage.set(1); + } + + toggleFilters() { + this.showFilters.update(v => !v); + } + onPageChange(page: number) { this.currentPage.set(page); } diff --git a/apps/frontend/src/app/components/shows/shows-list.component.ts b/apps/frontend/src/app/components/shows/shows-list.component.ts index c01f1a4..094351c 100644 --- a/apps/frontend/src/app/components/shows/shows-list.component.ts +++ b/apps/frontend/src/app/components/shows/shows-list.component.ts @@ -358,6 +358,51 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, Sugg } +
+ + + @if (searchQuery() || selectedTags().length > 0) { + + } +
+ + @if (showFilters()) { +
+
+

Filter by Tags

+
+ @for (tag of allTags(); track tag) { + + } + @empty { +

No tags available

+ } +
+
+
+ } +