+
-
{{ message() }}
+
{{ message() }}
diff --git a/frontend/src/app/shared/components/pagination/pagination.component.ts b/frontend/src/app/shared/components/pagination/pagination.component.ts
new file mode 100644
index 0000000..b59d193
--- /dev/null
+++ b/frontend/src/app/shared/components/pagination/pagination.component.ts
@@ -0,0 +1,334 @@
+import { Component, input, output } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { MatButtonModule } from '@angular/material/button';
+import { MatIconModule } from '@angular/material/icon';
+import { MatTooltipModule } from '@angular/material/tooltip';
+import { MatSelectModule } from '@angular/material/select';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { PaginationState } from '../../../core/services/pagination.service';
+
+/**
+ * Pagination Component
+ * Reusable pagination controls with customizable options
+ *
+ * Features:
+ * - Previous/Next buttons
+ * - First/Last page buttons
+ * - Page number buttons with active state
+ * - "Showing X-Y of Z results" display
+ * - Page size selector
+ * - Responsive design (fewer buttons on mobile)
+ * - Accessible with keyboard navigation and ARIA labels
+ *
+ * @example
+ *
+ *
+ */
+@Component({
+ selector: 'app-pagination',
+ imports: [
+ CommonModule,
+ MatButtonModule,
+ MatIconModule,
+ MatTooltipModule,
+ MatSelectModule,
+ MatFormFieldModule
+ ],
+ template: `
+
+ `,
+ styles: [`
+ .pagination-container {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ padding: 1rem;
+ background: var(--surface-color, #fff);
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ }
+
+ .pagination-info {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .info-text {
+ font-size: 0.875rem;
+ color: var(--text-secondary, #666);
+ }
+
+ .info-text strong {
+ color: var(--text-primary, #333);
+ font-weight: 600;
+ }
+
+ .pagination-actions {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ flex-wrap: wrap;
+ }
+
+ .page-size-selector {
+ min-width: 140px;
+ }
+
+ .page-size-selector ::ng-deep .mat-mdc-form-field-subscript-wrapper {
+ display: none;
+ }
+
+ .pagination-controls {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ }
+
+ .page-numbers {
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+ }
+
+ .page-button {
+ min-width: 40px;
+ height: 40px;
+ padding: 0 8px;
+ border-radius: 4px;
+ transition: all 0.2s ease;
+ }
+
+ .page-button:hover {
+ background-color: var(--hover-color, rgba(0, 0, 0, 0.04));
+ }
+
+ .page-button.active {
+ background-color: var(--primary-color, #1976d2);
+ color: white;
+ font-weight: 600;
+ }
+
+ .page-button.active:hover {
+ background-color: var(--primary-dark, #1565c0);
+ }
+
+ .ellipsis {
+ padding: 0 8px;
+ color: var(--text-secondary, #666);
+ user-select: none;
+ }
+
+ /* Responsive design */
+ @media (max-width: 768px) {
+ .pagination-container {
+ padding: 0.75rem;
+ }
+
+ .pagination-actions {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .page-size-selector {
+ width: 100%;
+ }
+
+ .pagination-controls {
+ justify-content: center;
+ }
+
+ .page-numbers {
+ gap: 0.125rem;
+ }
+
+ .page-button {
+ min-width: 36px;
+ height: 36px;
+ font-size: 0.875rem;
+ }
+
+ /* Hide ellipsis on very small screens */
+ @media (max-width: 480px) {
+ .ellipsis {
+ display: none;
+ }
+ }
+ }
+
+ /* Dark mode support */
+ @media (prefers-color-scheme: dark) {
+ .pagination-container {
+ background: var(--surface-dark, #1e1e1e);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
+ }
+
+ .info-text {
+ color: var(--text-secondary-dark, #b0b0b0);
+ }
+
+ .info-text strong {
+ color: var(--text-primary-dark, #e0e0e0);
+ }
+
+ .page-button:hover {
+ background-color: rgba(255, 255, 255, 0.08);
+ }
+
+ .ellipsis {
+ color: var(--text-secondary-dark, #b0b0b0);
+ }
+ }
+
+ /* Focus styles for accessibility */
+ .page-button:focus-visible,
+ button:focus-visible {
+ outline: 2px solid var(--primary-color, #1976d2);
+ outline-offset: 2px;
+ }
+
+ /* Disabled state */
+ button:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+ }
+ `]
+})
+export class PaginationComponent {
+ // Input signals
+ state = input.required
();
+ pageNumbers = input<(number | string)[]>([]);
+ pageSizeOptions = input([10, 25, 50, 100]);
+ showPageSizeSelector = input(true);
+ showFirstLast = input(true);
+ maxVisiblePages = input(5);
+ itemLabel = input('results');
+
+ // Output events
+ pageChange = output();
+ pageSizeChange = output();
+
+ /**
+ * Handle page change
+ */
+ onPageChange(page: number): void {
+ if (page >= 1 && page <= (this.state()?.totalPages ?? 1)) {
+ this.pageChange.emit(page);
+ }
+ }
+
+ /**
+ * Handle page button click (for page numbers)
+ */
+ handlePageClick(page: number | string): void {
+ if (typeof page === 'number') {
+ this.onPageChange(page);
+ }
+ }
+
+ /**
+ * Handle page size change
+ */
+ onPageSizeChange(pageSize: number): void {
+ this.pageSizeChange.emit(pageSize);
+ }
+}
diff --git a/frontend/src/app/shared/components/search/search.component.html b/frontend/src/app/shared/components/search/search.component.html
new file mode 100644
index 0000000..68714ab
--- /dev/null
+++ b/frontend/src/app/shared/components/search/search.component.html
@@ -0,0 +1,166 @@
+
+
+
+
+ search
+
+
+ @if (searchQuery()) {
+
+ close
+
+ }
+
+
+
+
+ @if (showDropdown()) {
+
+
+ @if (isSearching()) {
+
+ }
+
+
+ @else if (isEmptySearch()) {
+
+
search_off
+
No results found for "{{ searchQuery() }} "
+
Try different keywords or check your spelling
+
+ }
+
+
+ @else if (hasResults()) {
+
+
+ @if (searchResults().categories.length > 0) {
+
+
+
+ @for (category of searchResults().categories; track category.id) {
+
+
{{ category.icon || 'category' }}
+
+
+ @if (category.description) {
+
{{ category.description }}
+ }
+
+
chevron_right
+
+ }
+
+ }
+
+
+ @if (searchResults().questions.length > 0) {
+ @if (searchResults().categories.length > 0) {
+
+ }
+
+
+
+
+ @for (question of searchResults().questions; track question.id) {
+
+
quiz
+
+
+
+ @if (question.category) {
+
+ category
+ {{ question.category }}
+
+ }
+ @if (question.difficulty) {
+
+ {{ question.difficulty }}
+
+ }
+
+
+
chevron_right
+
+ }
+
+ }
+
+
+ @if (searchResults().quizzes.length > 0) {
+ @if (searchResults().categories.length > 0 || searchResults().questions.length > 0) {
+
+ }
+
+
+
+
+ @for (quiz of searchResults().quizzes; track quiz.id) {
+
+
assessment
+
+
{{ quiz.title }}
+ @if (quiz.description) {
+
{{ quiz.description }}
+ }
+
+
chevron_right
+
+ }
+
+ }
+
+
+
+
+
+ open_in_new
+ See all {{ searchResults().totalResults }} results
+
+
+
+ }
+
+ }
+
diff --git a/frontend/src/app/shared/components/search/search.component.scss b/frontend/src/app/shared/components/search/search.component.scss
new file mode 100644
index 0000000..f2cdf89
--- /dev/null
+++ b/frontend/src/app/shared/components/search/search.component.scss
@@ -0,0 +1,352 @@
+.search-container {
+ position: relative;
+ width: 100%;
+ max-width: 600px;
+
+ @media (max-width: 768px) {
+ max-width: 100%;
+ }
+}
+
+// Search Input
+.search-input-wrapper {
+ position: relative;
+
+ .search-field {
+ width: 100%;
+
+ ::ng-deep {
+ .mat-mdc-form-field-infix {
+ padding: 0.5rem 0;
+ }
+
+ .mat-mdc-text-field-wrapper {
+ padding: 0;
+ }
+
+ input {
+ font-size: 0.95rem;
+ }
+
+ .mat-mdc-form-field-subscript-wrapper {
+ display: none;
+ }
+ }
+ }
+}
+
+// Search Dropdown
+.search-dropdown {
+ position: absolute;
+ top: calc(100% + 0.5rem);
+ left: 0;
+ right: 0;
+ background: var(--surface-color);
+ border-radius: 8px;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
+ max-height: 500px;
+ overflow-y: auto;
+ z-index: 1000;
+ animation: dropdownSlide 0.2s ease-out;
+
+ @media (max-width: 768px) {
+ max-height: 400px;
+ }
+
+ @keyframes dropdownSlide {
+ from {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ }
+
+ // Custom scrollbar
+ &::-webkit-scrollbar {
+ width: 6px;
+ }
+
+ &::-webkit-scrollbar-track {
+ background: transparent;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: var(--scrollbar-color);
+ border-radius: 3px;
+
+ &:hover {
+ background: var(--scrollbar-hover-color);
+ }
+ }
+}
+
+// Loading State
+.search-loading {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 3rem 2rem;
+ gap: 1rem;
+
+ p {
+ margin: 0;
+ font-size: 0.95rem;
+ color: var(--text-secondary);
+ }
+}
+
+// Empty State
+.search-empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 3rem 2rem;
+ gap: 0.75rem;
+ text-align: center;
+
+ mat-icon {
+ font-size: 3rem;
+ width: 3rem;
+ height: 3rem;
+ color: var(--text-disabled);
+ }
+
+ p {
+ margin: 0;
+ font-size: 0.95rem;
+ color: var(--text-primary);
+
+ strong {
+ color: var(--primary-color);
+ }
+ }
+
+ .hint {
+ font-size: 0.875rem;
+ color: var(--text-secondary);
+ }
+}
+
+// Results
+.search-results {
+ padding: 0.5rem 0;
+}
+
+.results-section {
+ padding: 0.5rem 0;
+
+ .section-header {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.75rem 1rem;
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+
+ mat-icon {
+ font-size: 1.25rem;
+ width: 1.25rem;
+ height: 1.25rem;
+ }
+
+ .count {
+ margin-left: auto;
+ padding: 0.125rem 0.5rem;
+ background-color: var(--chip-background);
+ border-radius: 12px;
+ font-size: 0.75rem;
+ }
+ }
+}
+
+// Result Item
+.result-item {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 0.75rem 1rem;
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+
+ &:hover,
+ &.selected {
+ background-color: var(--hover-background);
+ }
+
+ &.selected {
+ border-left: 3px solid var(--primary-color);
+ padding-left: calc(1rem - 3px);
+ }
+
+ .result-icon {
+ flex-shrink: 0;
+ font-size: 1.5rem;
+ width: 1.5rem;
+ height: 1.5rem;
+ color: var(--icon-color);
+ }
+
+ .result-content {
+ flex: 1;
+ min-width: 0;
+
+ .result-title {
+ font-size: 0.95rem;
+ font-weight: 500;
+ color: var(--text-primary);
+ line-height: 1.4;
+ margin-bottom: 0.25rem;
+
+ // Truncate long titles
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+
+ ::ng-deep mark {
+ background-color: var(--highlight-background);
+ color: var(--highlight-text);
+ padding: 0.125rem 0.25rem;
+ border-radius: 3px;
+ font-weight: 600;
+ }
+ }
+
+ .result-description {
+ font-size: 0.875rem;
+ color: var(--text-secondary);
+ line-height: 1.4;
+ margin-top: 0.25rem;
+
+ // Truncate long descriptions
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ }
+
+ .result-meta {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ margin-top: 0.5rem;
+ flex-wrap: wrap;
+
+ .meta-item {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.25rem;
+ font-size: 0.875rem;
+ color: var(--text-secondary);
+
+ mat-icon {
+ font-size: 1rem;
+ width: 1rem;
+ height: 1rem;
+ }
+ }
+
+ .difficulty-chip {
+ height: 20px;
+ font-size: 0.75rem;
+ padding: 0 0.5rem;
+
+ ::ng-deep .mdc-evolution-chip__action--primary {
+ padding: 0 0.5rem;
+ }
+ }
+ }
+ }
+
+ .navigate-icon {
+ flex-shrink: 0;
+ font-size: 1.25rem;
+ width: 1.25rem;
+ height: 1.25rem;
+ color: var(--text-disabled);
+ }
+}
+
+// See All Link
+.see-all-link {
+ padding: 0.5rem 1rem;
+ text-align: center;
+
+ button {
+ width: 100%;
+ justify-content: center;
+
+ mat-icon {
+ margin-right: 0.5rem;
+ }
+ }
+}
+
+// Dark Mode
+@media (prefers-color-scheme: dark) {
+ .search-container {
+ --surface-color: #1e1e1e;
+ --text-primary: #e0e0e0;
+ --text-secondary: #a0a0a0;
+ --text-disabled: #606060;
+ --primary-color: #2196f3;
+ --icon-color: #90caf9;
+ --hover-background: rgba(255, 255, 255, 0.08);
+ --chip-background: rgba(255, 255, 255, 0.1);
+ --highlight-background: rgba(33, 150, 243, 0.3);
+ --highlight-text: #ffffff;
+ --scrollbar-color: rgba(255, 255, 255, 0.2);
+ --scrollbar-hover-color: rgba(255, 255, 255, 0.3);
+ }
+}
+
+// Light Mode
+@media (prefers-color-scheme: light) {
+ .search-container {
+ --surface-color: #ffffff;
+ --text-primary: #212121;
+ --text-secondary: #757575;
+ --text-disabled: #bdbdbd;
+ --primary-color: #1976d2;
+ --icon-color: #1976d2;
+ --hover-background: rgba(0, 0, 0, 0.04);
+ --chip-background: rgba(0, 0, 0, 0.08);
+ --highlight-background: rgba(33, 150, 243, 0.2);
+ --highlight-text: #0d47a1;
+ --scrollbar-color: rgba(0, 0, 0, 0.2);
+ --scrollbar-hover-color: rgba(0, 0, 0, 0.3);
+ }
+}
+
+// Accessibility
+@media (prefers-reduced-motion: reduce) {
+ .search-dropdown {
+ animation: none;
+ }
+
+ .result-item {
+ transition: none;
+ }
+}
+
+// Focus styles
+.search-field {
+ ::ng-deep input:focus {
+ outline: none;
+ }
+}
+
+.result-item:focus {
+ outline: 2px solid var(--primary-color);
+ outline-offset: -2px;
+}
diff --git a/frontend/src/app/shared/components/search/search.component.ts b/frontend/src/app/shared/components/search/search.component.ts
new file mode 100644
index 0000000..c099158
--- /dev/null
+++ b/frontend/src/app/shared/components/search/search.component.ts
@@ -0,0 +1,313 @@
+import { Component, OnInit, inject, signal, effect, ViewChild, ElementRef } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { Router } from '@angular/router';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+import { MatIconModule } from '@angular/material/icon';
+import { MatButtonModule } from '@angular/material/button';
+import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
+import { MatDividerModule } from '@angular/material/divider';
+import { MatChipsModule } from '@angular/material/chips';
+import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
+import { Subject } from 'rxjs';
+import { SearchService, SearchResultItem, SearchResultType } from '../../../core/services/search.service';
+
+/**
+ * SearchComponent
+ *
+ * Global search component for searching across questions, categories, and quizzes.
+ *
+ * Features:
+ * - Debounced search input (500ms)
+ * - Dropdown results with grouped sections
+ * - Keyboard navigation (arrow keys, enter, escape)
+ * - Click outside to close
+ * - Loading indicator
+ * - Empty state
+ * - Result highlighting
+ * - "See All Results" link
+ * - Responsive design
+ */
+@Component({
+ selector: 'app-search',
+ standalone: true,
+ imports: [
+ CommonModule,
+ FormsModule,
+ MatFormFieldModule,
+ MatInputModule,
+ MatIconModule,
+ MatButtonModule,
+ MatProgressSpinnerModule,
+ MatDividerModule,
+ MatChipsModule
+ ],
+ templateUrl: './search.component.html',
+ styleUrl: './search.component.scss'
+})
+export class SearchComponent implements OnInit {
+ private readonly searchService = inject(SearchService);
+ private readonly router = inject(Router);
+
+ @ViewChild('searchInput') searchInput!: ElementRef;
+
+ // Service signals
+ readonly searchResults = this.searchService.searchResults;
+ readonly isSearching = this.searchService.isSearching;
+ readonly hasSearched = this.searchService.hasSearched;
+
+ // Component state
+ readonly searchQuery = signal('');
+ readonly showDropdown = signal(false);
+ readonly selectedIndex = signal(-1);
+
+ // Search input subject for debouncing
+ private searchSubject = new Subject();
+
+ // Flattened results for keyboard navigation
+ private flatResults: SearchResultItem[] = [];
+
+ ngOnInit(): void {
+ this.setupSearchDebounce();
+ this.setupClickOutside();
+ }
+
+ /**
+ * Setup debounced search
+ */
+ private setupSearchDebounce(): void {
+ this.searchSubject
+ .pipe(
+ debounceTime(500),
+ distinctUntilChanged()
+ )
+ .subscribe((query) => {
+ if (query.trim().length >= 2) {
+ this.searchService.search(query).subscribe(() => {
+ this.showDropdown.set(true);
+ this.updateFlatResults();
+ });
+ } else {
+ this.clearSearch();
+ }
+ });
+ }
+
+ /**
+ * Setup click outside to close dropdown
+ */
+ private setupClickOutside(): void {
+ document.addEventListener('click', (event) => {
+ const target = event.target as HTMLElement;
+ const searchContainer = document.querySelector('.search-container');
+
+ if (searchContainer && !searchContainer.contains(target)) {
+ this.closeDropdown();
+ }
+ });
+ }
+
+ /**
+ * Handle search input change
+ */
+ onSearchInput(query: string): void {
+ this.searchQuery.set(query);
+ this.searchSubject.next(query);
+
+ if (query.trim().length < 2) {
+ this.closeDropdown();
+ }
+ }
+
+ /**
+ * Handle keyboard navigation
+ */
+ onKeyDown(event: KeyboardEvent): void {
+ const key = event.key;
+
+ if (!this.showDropdown()) return;
+
+ switch (key) {
+ case 'ArrowDown':
+ event.preventDefault();
+ this.navigateDown();
+ break;
+ case 'ArrowUp':
+ event.preventDefault();
+ this.navigateUp();
+ break;
+ case 'Enter':
+ event.preventDefault();
+ this.selectCurrentResult();
+ break;
+ case 'Escape':
+ event.preventDefault();
+ this.closeDropdown();
+ break;
+ }
+ }
+
+ /**
+ * Navigate down in results
+ */
+ private navigateDown(): void {
+ const maxIndex = this.flatResults.length - 1;
+ if (this.selectedIndex() < maxIndex) {
+ this.selectedIndex.update(i => i + 1);
+ this.scrollToSelected();
+ }
+ }
+
+ /**
+ * Navigate up in results
+ */
+ private navigateUp(): void {
+ if (this.selectedIndex() > 0) {
+ this.selectedIndex.update(i => i - 1);
+ this.scrollToSelected();
+ }
+ }
+
+ /**
+ * Select current highlighted result
+ */
+ private selectCurrentResult(): void {
+ const index = this.selectedIndex();
+ if (index >= 0 && index < this.flatResults.length) {
+ this.navigateToResult(this.flatResults[index]);
+ }
+ }
+
+ /**
+ * Scroll to selected result in dropdown
+ */
+ private scrollToSelected(): void {
+ setTimeout(() => {
+ const selected = document.querySelector('.result-item.selected');
+ if (selected) {
+ selected.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
+ }
+ }, 0);
+ }
+
+ /**
+ * Update flat results array for keyboard navigation
+ */
+ private updateFlatResults(): void {
+ const results = this.searchResults();
+ this.flatResults = [
+ ...results.categories,
+ ...results.questions,
+ ...results.quizzes
+ ];
+ this.selectedIndex.set(-1);
+ }
+
+ /**
+ * Navigate to search result
+ */
+ navigateToResult(result: SearchResultItem): void {
+ if (result.url) {
+ this.router.navigate([result.url]);
+ this.closeDropdown();
+ this.clearSearch();
+ }
+ }
+
+ /**
+ * Navigate to full search results page
+ */
+ viewAllResults(): void {
+ this.router.navigate(['/search'], {
+ queryParams: { q: this.searchQuery() }
+ });
+ this.closeDropdown();
+ }
+
+ /**
+ * Clear search
+ */
+ clearSearch(): void {
+ this.searchQuery.set('');
+ this.searchService.clearResults();
+ this.showDropdown.set(false);
+ this.selectedIndex.set(-1);
+ this.flatResults = [];
+ }
+
+ /**
+ * Close dropdown
+ */
+ closeDropdown(): void {
+ this.showDropdown.set(false);
+ this.selectedIndex.set(-1);
+ }
+
+ /**
+ * Focus search input
+ */
+ focusSearch(): void {
+ this.searchInput?.nativeElement.focus();
+ }
+
+ /**
+ * Get icon for result type
+ */
+ getTypeIcon(type: SearchResultType): string {
+ switch (type) {
+ case 'question':
+ return 'quiz';
+ case 'category':
+ return 'category';
+ case 'quiz':
+ return 'assessment';
+ default:
+ return 'search';
+ }
+ }
+
+ /**
+ * Get chip color for difficulty
+ */
+ getDifficultyColor(difficulty?: string): string {
+ if (!difficulty) return '';
+
+ switch (difficulty.toLowerCase()) {
+ case 'easy':
+ return 'primary';
+ case 'medium':
+ return 'accent';
+ case 'hard':
+ return 'warn';
+ default:
+ return '';
+ }
+ }
+
+ /**
+ * Check if result is selected
+ */
+ isSelected(result: SearchResultItem): boolean {
+ const index = this.selectedIndex();
+ if (index < 0) return false;
+
+ return this.flatResults[index]?.id === result.id &&
+ this.flatResults[index]?.type === result.type;
+ }
+
+ /**
+ * Check if there are any results
+ */
+ hasResults(): boolean {
+ const results = this.searchResults();
+ return results.totalResults > 0;
+ }
+
+ /**
+ * Check if search is empty
+ */
+ isEmptySearch(): boolean {
+ return this.hasSearched() && !this.hasResults() && !this.isSearching();
+ }
+}