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