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
@@ -397,6 +397,54 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti
</form>
}
<div class="search-section">
<input
type="text"
[(ngModel)]="searchQuery"
(ngModelChange)="onSearchChange()"
placeholder="Search by title, author, ISBN, or notes..."
class="search-input"
>
<button
(click)="toggleFilters()"
class="btn btn-secondary"
[class.active]="showFilters()"
>
<span>🔧 Advanced Filters</span>
@if (selectedTags().length > 0) {
<span class="filter-badge">{{ selectedTags().length }}</span>
}
</button>
</div>
@if (showFilters()) {
<div class="advanced-filters">
<div class="filter-group">
<label>Filter by Tags:</label>
<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 class="tag-label">{{ tag }}</span>
</label>
}
@empty {
<p class="no-tags">No tags available</p>
}
</div>
@if (selectedTags().length > 0) {
<button (click)="clearTags()" class="btn btn-sm btn-secondary clear-tags">
Clear All Tags
</button>
}
</div>
</div>
}
<div class="filters">
<button
(click)="setFilter('all')"
@@ -680,10 +728,125 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti
margin-top: 1rem;
}
.search-section {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
align-items: center;
}
.search-input {
flex: 1;
padding: 0.75rem 1rem;
border: 2px solid var(--witch-lavender);
border-radius: 4px;
font-size: 1rem;
background-color: var(--witch-moon);
color: var(--witch-purple);
transition: border-color 0.3s;
}
.search-input:focus {
outline: none;
border-color: var(--witch-rose);
box-shadow: 0 0 0 3px rgba(168, 87, 126, 0.2);
}
.search-input::placeholder {
color: var(--witch-mauve);
}
.advanced-filters {
background: rgba(255, 255, 255, 0.95);
border: 2px solid var(--witch-lavender);
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1.5rem;
backdrop-filter: blur(10px);
}
.filter-group {
margin-bottom: 1rem;
}
.filter-group:last-child {
margin-bottom: 0;
}
.filter-group label {
display: block;
margin-bottom: 0.75rem;
font-weight: 600;
color: var(--witch-purple);
}
.tags-filter {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin-bottom: 1rem;
}
.tag-checkbox {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
padding: 0.25rem 0.5rem;
border: 2px solid var(--witch-lavender);
border-radius: 4px;
transition: all 0.2s;
font-weight: 500;
}
.tag-checkbox:hover {
background: var(--witch-lavender);
transform: translateY(-1px);
}
.tag-checkbox input[type="checkbox"] {
cursor: pointer;
}
.tag-checkbox input[type="checkbox"]:checked + .tag-label {
color: #8b6f47;
}
.tag-checkbox:has(input:checked) {
background: var(--witch-lavender);
border-color: #8b6f47;
}
.tag-label {
font-size: 0.9rem;
color: var(--witch-plum);
}
.no-tags {
color: var(--witch-mauve);
font-style: italic;
margin: 0;
}
.clear-tags {
margin-top: 0.5rem;
}
.filter-badge {
background: var(--witch-rose);
color: var(--witch-moon);
padding: 0.1rem 0.4rem;
border-radius: 10px;
font-size: 0.75rem;
font-weight: 600;
margin-left: 0.25rem;
}
.filters {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.filter-btn {
@@ -1175,6 +1338,25 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti
background: rgba(139, 111, 71, 0.2);
text-decoration: underline;
}
@media (max-width: 768px) {
.search-section {
flex-direction: column;
}
.search-input {
width: 100%;
}
.filters {
justify-content: center;
}
.filter-btn {
font-size: 0.85rem;
padding: 0.4rem 0.8rem;
}
}
`]
})
export class BooksListComponent implements OnInit {
@@ -1189,6 +1371,9 @@ export class BooksListComponent implements OnInit {
showAddForm = signal(false);
editingBook = signal<Book | null>(null);
statusFilter = signal<'all' | BookStatus>('all');
searchQuery = signal('');
selectedTags = signal<string[]>([]);
showFilters = signal(false);
// Pagination state
currentPage = signal(1);
@@ -1227,11 +1412,43 @@ export class BooksListComponent implements OnInit {
finishedCount = computed(() => this.books().filter(book => book.status === BookStatus.finished).length);
toReadCount = computed(() => this.books().filter(book => book.status === BookStatus.toRead).length);
// Get all unique tags from all books
allTags = computed(() => {
const tags = new Set<string>();
this.books().forEach(book => {
book.tags?.forEach(tag => tags.add(tag));
});
return Array.from(tags).sort();
});
filteredBooks = computed(() => {
const filter = this.statusFilter();
const books = filter === 'all'
? this.books()
: this.books().filter(book => book.status === filter);
let books = this.books();
// Apply status filter
const statusFilter = this.statusFilter();
if (statusFilter !== 'all') {
books = books.filter(book => book.status === statusFilter);
}
// Apply search filter
const searchQuery = this.searchQuery().toLowerCase().trim();
if (searchQuery) {
books = books.filter(book =>
book.title.toLowerCase().includes(searchQuery) ||
book.author.toLowerCase().includes(searchQuery) ||
book.isbn?.toLowerCase().includes(searchQuery) ||
book.notes?.toLowerCase().includes(searchQuery)
);
}
// Apply tag filter
const selectedTags = this.selectedTags();
if (selectedTags.length > 0) {
books = books.filter(book =>
selectedTags.every(tag => book.tags?.includes(tag))
);
}
return books;
});
@@ -1287,6 +1504,31 @@ export class BooksListComponent implements OnInit {
this.currentPage.set(1); // Reset to first page when filter changes
}
onSearchChange() {
this.currentPage.set(1); // Reset to first page when search changes
}
toggleFilters() {
this.showFilters.update(show => !show);
}
toggleTag(tag: string) {
this.selectedTags.update(tags => {
const index = tags.indexOf(tag);
if (index === -1) {
return [...tags, tag];
} else {
return tags.filter(t => t !== tag);
}
});
this.currentPage.set(1); // Reset to first page when tags change
}
clearTags() {
this.selectedTags.set([]);
this.currentPage.set(1); // Reset to first page when tags are cleared
}
onPageChange(page: number) {
this.currentPage.set(page);
}