generated from nhcarrigan/template
feat: add ability to search
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user