Added a minor loading animation when searching.
This commit is contained in:
@ -30,7 +30,8 @@
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.results-error img, .results-end img {
|
||||
.results-error img,
|
||||
.results-end img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@ -40,4 +41,32 @@
|
||||
|
||||
.filter-warning {
|
||||
filter: brightness(0) saturate(100%) invert(52%) sepia(95%) saturate(2039%) hue-rotate(3deg) brightness(106%) contrast(102%);
|
||||
}
|
||||
|
||||
.loading {
|
||||
width: fit-content;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
font-size: 25px;
|
||||
align-self: center;
|
||||
padding: 5px;
|
||||
margin: 20px;
|
||||
background: radial-gradient(circle closest-side, #000 94%, #0000) right/calc(200% - 1em) 100%;
|
||||
animation: l24 1s infinite alternate linear;
|
||||
}
|
||||
|
||||
.loading::before {
|
||||
content: "Loading...";
|
||||
line-height: 1em;
|
||||
color: #0000;
|
||||
background: inherit;
|
||||
background-image: radial-gradient(circle closest-side, #fff 94%, #000);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
@keyframes l24 {
|
||||
100% {
|
||||
background-position: left
|
||||
}
|
||||
}
|
@ -1,21 +1,24 @@
|
||||
<div class="search-content">
|
||||
<search-box
|
||||
(searchOutput)="search.next($event)"
|
||||
(filtersOutput)="filters.next($event)" />
|
||||
<div #scrollbar
|
||||
class="search-content">
|
||||
<search-box (searchOutput)="search.next($event)"
|
||||
(filtersOutput)="filters.next($event)" />
|
||||
<div class="results-box"
|
||||
(scroll)="onResultsScroll($event)">
|
||||
@for (result of results; track $index) {
|
||||
<media-search-item class="result-item"
|
||||
[media]="result" />
|
||||
}
|
||||
@if (busy()) {
|
||||
<div class="loading"></div>
|
||||
}
|
||||
@if (searchError() != null) {
|
||||
<p class="results-error">
|
||||
<img src="/icons/error_icon.svg"
|
||||
alt="error icon"
|
||||
class="filter-error" />
|
||||
{{searchError()}}
|
||||
</p>
|
||||
}
|
||||
<p class="results-error">
|
||||
<img src="/icons/error_icon.svg"
|
||||
alt="error icon"
|
||||
class="filter-error" />
|
||||
{{searchError()}}
|
||||
</p>
|
||||
}
|
||||
@if (endOfResults()) {
|
||||
<div class="results-end">
|
||||
<img src="/icons/warning_icon.svg"
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { Component, inject, NgZone, OnDestroy, signal } from '@angular/core';
|
||||
import { AuthService } from '../../services/auth/auth.service';
|
||||
import { Component, ElementRef, inject, NgZone, OnDestroy, signal, ViewChild } from '@angular/core';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { BehaviorSubject, combineLatest, distinctUntilChanged, filter, scan, Subscription, tap, throttleTime } from 'rxjs';
|
||||
import { BehaviorSubject, combineLatest, debounceTime, distinctUntilChanged, filter, scan, Subscription, throttleTime } from 'rxjs';
|
||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||
import { RedirectionService } from '../../services/redirection.service';
|
||||
import { BookSearchResultDto } from '../../shared/dto/book-search-result.dto';
|
||||
import { MediaSearchItemComponent } from '../media-search-item/media-search-item.component';
|
||||
import { SearchBoxComponent } from "../search-box/search-box.component";
|
||||
@ -21,17 +19,18 @@ import { SearchContextDto } from '../../shared/dto/search-context.dto';
|
||||
styleUrl: './add-new-page.component.css'
|
||||
})
|
||||
export class AddNewPageComponent implements OnDestroy {
|
||||
private readonly _auth = inject(AuthService);
|
||||
private readonly _http = inject(HttpClient);
|
||||
private readonly _redirect = inject(RedirectionService);
|
||||
private readonly _subscriptions: Subscription[] = [];
|
||||
private readonly _zone = inject(NgZone);
|
||||
|
||||
@ViewChild('scrollbar') private readonly searchContentRef: ElementRef<Element> = {} as ElementRef;
|
||||
|
||||
search = new BehaviorSubject<string>('');
|
||||
filters = new BehaviorSubject<SearchContextDto>(new SearchContextDto());
|
||||
page = new BehaviorSubject<number>(0);
|
||||
results: BookSearchResultDto[] = [];
|
||||
resultsPerPage = signal<number>(10);
|
||||
busy = signal<boolean>(false);
|
||||
endOfResults = signal<boolean>(false);
|
||||
searchError = signal<string | null>(null);
|
||||
|
||||
@ -45,13 +44,14 @@ export class AddNewPageComponent implements OnDestroy {
|
||||
),
|
||||
filters: this.filters,
|
||||
page: this.page.pipe(
|
||||
throttleTime(1500, undefined, { leading: true, trailing: true }),
|
||||
throttleTime(1500, undefined, { leading: false, trailing: true }),
|
||||
),
|
||||
}).pipe(
|
||||
filter(entry => entry.search!.length > 1),
|
||||
throttleTime(1000, undefined, { leading: false, trailing: true }),
|
||||
debounceTime(1000),
|
||||
scan((acc, next) => {
|
||||
// New searches means resetting to page 0.
|
||||
// Ignore if user is busy loading new pages.
|
||||
if (acc.search != next.search) {
|
||||
this.results = [];
|
||||
return {
|
||||
@ -65,8 +65,10 @@ export class AddNewPageComponent implements OnDestroy {
|
||||
return acc;
|
||||
}
|
||||
|
||||
// Ignore further searching on the same search term.
|
||||
if (this.endOfResults()) {
|
||||
// Ignore further page searching if:
|
||||
// - there are no more results;
|
||||
// - user is still busy loading new pages.
|
||||
if (this.endOfResults() || this.busy()) {
|
||||
return {
|
||||
...next,
|
||||
page: -1,
|
||||
@ -79,10 +81,12 @@ export class AddNewPageComponent implements OnDestroy {
|
||||
page: Math.min(acc.page + 1, next.page),
|
||||
};
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
filter(entry => entry.page >= 0),
|
||||
tap(_ => this.endOfResults.set(false)),
|
||||
distinctUntilChanged(),
|
||||
).subscribe((entry) => {
|
||||
this.busy.set(true);
|
||||
this.endOfResults.set(false);
|
||||
this.searchContentRef.nativeElement.scrollTop = 0;
|
||||
this._http.get('/api/providers/search',
|
||||
{
|
||||
params: {
|
||||
@ -99,8 +103,10 @@ export class AddNewPageComponent implements OnDestroy {
|
||||
if (results.length < this.resultsPerPage()) {
|
||||
this.endOfResults.set(true);
|
||||
}
|
||||
this.busy.set(false);
|
||||
},
|
||||
error: (err) => {
|
||||
this.busy.set(false);
|
||||
if (err instanceof HttpErrorResponse) {
|
||||
if (err.status == 400) {
|
||||
this.searchError.set('Something went wrong when Google received the request.');
|
||||
@ -128,7 +134,7 @@ export class AddNewPageComponent implements OnDestroy {
|
||||
const limit = scroll.scrollHeight - scroll.clientHeight;
|
||||
|
||||
// Prevent page changes when:
|
||||
// - new search is happening (emptying results);
|
||||
// - new search is happening (emptied results);
|
||||
// - still scrolling through current content.
|
||||
if (scroll.scrollTop == 0 || scroll.scrollTop < limit - 25) {
|
||||
return;
|
||||
|
Reference in New Issue
Block a user