feat: add ability to search

This commit is contained in:
2026-02-04 20:37:51 -08:00
parent ca288eaac4
commit a9764a4a82
7 changed files with 1149 additions and 23 deletions
@@ -364,6 +364,51 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment, Suggestion
</form>
}
<div class="search-section">
<input
type="text"
[value]="searchQuery()"
(input)="searchQuery.set($any($event.target).value); currentPage.set(1)"
name="search"
placeholder="Search by title, author, or notes..."
class="search-input"
>
<button (click)="toggleFilters()" class="btn btn-secondary btn-sm">
{{ showFilters() ? 'Hide' : 'Show' }} Advanced Filters
@if (selectedTags().length > 0) {
({{ selectedTags().length }})
}
</button>
@if (searchQuery() || selectedTags().length > 0) {
<button (click)="clearFilters()" class="btn btn-secondary btn-sm">
Clear All Filters
</button>
}
</div>
@if (showFilters()) {
<div class="advanced-filters">
<div class="filter-group">
<h4>Filter by Tags</h4>
<div class="tags-filter">
@for (tag of allTags(); track tag) {
<label class="tag-checkbox">
<input
type="checkbox"
[checked]="selectedTags().includes(tag)"
(change)="toggleTag(tag)"
>
<span>{{ tag }}</span>
</label>
}
@empty {
<p class="no-tags">No tags available</p>
}
</div>
</div>
</div>
}
<div class="filters">
<button
(click)="setFilter('all')"
@@ -621,6 +666,82 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment, Suggestion
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: #00b894;
}
.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: #00b894;
background: #f5fffc;
}
.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;
gap: 1rem;
@@ -1057,6 +1178,11 @@ export class MangaListComponent implements OnInit {
currentPage = signal(1);
pageSize = signal(25);
// Search and filter state
searchQuery = signal('');
selectedTags = signal<string[]>([]);
showFilters = signal(false);
comments = signal<Record<string, Comment[]>>({});
commentsLoading = signal<Record<string, boolean>>({});
expandedComments = signal<Record<string, boolean>>({});
@@ -1085,12 +1211,42 @@ export class MangaListComponent implements OnInit {
completedCount = computed(() => this.mangaList().filter(m => m.status === MangaStatus.completed).length);
wantToReadCount = computed(() => this.mangaList().filter(m => m.status === MangaStatus.wantToRead).length);
allTags = computed(() => {
const tagsSet = new Set<string>();
this.mangaList().forEach(manga => {
manga.tags?.forEach(tag => tagsSet.add(tag));
});
return Array.from(tagsSet).sort();
});
filteredManga = computed(() => {
const filter = this.statusFilter();
if (filter === 'all') {
return this.mangaList();
let manga = this.mangaList();
// Apply status filter
const statusFilter = this.statusFilter();
if (statusFilter !== 'all') {
manga = manga.filter(m => m.status === statusFilter);
}
return this.mangaList().filter(m => m.status === filter);
// Apply search filter
const searchQuery = this.searchQuery().toLowerCase().trim();
if (searchQuery) {
manga = manga.filter(m =>
m.title.toLowerCase().includes(searchQuery) ||
m.author.toLowerCase().includes(searchQuery) ||
m.notes?.toLowerCase().includes(searchQuery)
);
}
// Apply tag filter
const selectedTags = this.selectedTags();
if (selectedTags.length > 0) {
manga = manga.filter(m =>
selectedTags.every(tag => m.tags?.includes(tag))
);
}
return manga;
});
paginatedManga = computed(() => {
@@ -1144,6 +1300,27 @@ export class MangaListComponent 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.statusFilter.set('all');
this.currentPage.set(1);
}
toggleFilters() {
this.showFilters.update(v => !v);
}
onPageChange(page: number) {
this.currentPage.set(page);
}