diff --git a/frontend/public/off.html b/frontend/public/off.html
index 2b3e4b3c..89ccac0f 100644
--- a/frontend/public/off.html
+++ b/frontend/public/off.html
@@ -43,10 +43,10 @@
color: rgba(0,0,0,.75);
}
/* search results styling */
- searchalicious-results::part(results) {
+ searchalicious-results::part(results), searchalicious-results::part(results-loading) {
list-style: none;
display: grid;
- gap: 5px;
+ gap: 10px;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
margin: 0;
padding: 0;
diff --git a/frontend/src/mixins/search-ctl.ts b/frontend/src/mixins/search-ctl.ts
index 54a026c1..59e64e11 100644
--- a/frontend/src/mixins/search-ctl.ts
+++ b/frontend/src/mixins/search-ctl.ts
@@ -26,7 +26,7 @@ import {
SearchaliciousHistoryMixin,
} from './history';
import {SearchaliciousChart} from '../search-chart';
-import {canResetSearch, isSearchChanged} from '../signals';
+import {canResetSearch, isSearchChanged, isSearchLoading} from '../signals';
import {SignalWatcher} from '@lit-labs/preact-signals';
import {isTheSameSearchName} from '../utils/search';
@@ -490,6 +490,9 @@ export const SearchaliciousSearchMixin = >(
async search(page = 1) {
const {searchUrl, method, params, history} = this._searchUrl(page);
setCurrentURLHistory(history);
+
+ isSearchLoading(this.name).value = true;
+
let response;
if (method === 'GET') {
response = await fetch(
@@ -512,6 +515,7 @@ export const SearchaliciousSearchMixin = >(
this._currentPage = data.page;
this._pageCount = data.page_count;
+ isSearchLoading(this.name).value = false;
this.updateSearchSignals();
// dispatch an event with the results
diff --git a/frontend/src/search-results.ts b/frontend/src/search-results.ts
index f87e4138..9e33557f 100644
--- a/frontend/src/search-results.ts
+++ b/frontend/src/search-results.ts
@@ -1,4 +1,4 @@
-import {LitElement, html} from 'lit';
+import {LitElement, html, css} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
import {repeat} from 'lit/directives/repeat.js';
@@ -9,6 +9,8 @@ import {
MultipleResultTemplateError,
} from './errors';
import {localized, msg} from '@lit/localize';
+import {SignalWatcher} from '@lit-labs/preact-signals';
+import {isSearchLoading} from './signals';
// we need it to declare functions
type htmlType = typeof html;
@@ -23,9 +25,31 @@ type htmlType = typeof html;
*/
@customElement('searchalicious-results')
@localized()
-export class SearchaliciousResults extends SearchaliciousResultCtlMixin(
- LitElement
+export class SearchaliciousResults extends SignalWatcher(
+ SearchaliciousResultCtlMixin(LitElement)
) {
+ static override styles = css`
+ .loading {
+ height: 300px;
+ border-radius: 8px;
+ animation: loading 2.25s ease infinite;
+ box-shadow: 0 4px 4px rgba(0, 0, 0, 0.25);
+ background-color: var(--first-loading-color, #cacaca);
+ }
+
+ @keyframes loading {
+ 0% {
+ background-color: var(--first-loading-color, #cacaca);
+ }
+ 50% {
+ background-color: var(--second-loading-color, #bbbbbb);
+ }
+ 100% {
+ background-color: var(--first-loading-color, #cacaca);
+ }
+ }
+ `;
+
// the last search results
@state()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -35,6 +59,16 @@ export class SearchaliciousResults extends SearchaliciousResultCtlMixin(
@property({attribute: 'result-id'})
resultId = '';
+ @property({attribute: 'loadind-card-size', type: Number})
+ loadingCardSize = 200;
+
+ /**
+ * The parent width, used to compute the number of loading cards to display
+ */
+ get parentWidth() {
+ return this.parentElement?.offsetWidth || 1200;
+ }
+
/**
* A function rendering a single result. We define this just to get it's prototype right.
*
@@ -109,9 +143,31 @@ export class SearchaliciousResults extends SearchaliciousResultCtlMixin(
return slots[0].innerHTML;
}
+ /**
+ * Render the loading cards
+ * We display 2 columns of loading cards
+ */
+ renderLoading() {
+ // we take the row width and display 2 columns of loading cards
+ const numCols = Math.floor(this.parentWidth / this.loadingCardSize) * 2;
+
+ return html`
+
+
+ ${Array(numCols)
+ .fill(0)
+ .map(() => html``)}
+
+
+ `;
+ }
+
override render() {
if (this.results.length) {
return this.renderResults();
+ // if we are loading, we display the loading cards
+ } else if (isSearchLoading(this.searchName).value) {
+ return this.renderLoading();
} else if (this.searchLaunched) {
return html`${this.noResults}`;
} else {
diff --git a/frontend/src/signals.ts b/frontend/src/signals.ts
index f98fb8e0..6b536b51 100644
--- a/frontend/src/signals.ts
+++ b/frontend/src/signals.ts
@@ -18,6 +18,14 @@ const _isSearchChanged: Record = {} as Record<
Signal
>;
+/**
+ * Signals to indicate if the search is loading.
+ */
+const _isSearchLoading: Record = {} as Record<
+ string,
+ Signal
+>;
+
/**
* Function to get or create a signal by search name.
* If the signal does not exist, it creates it.
@@ -37,6 +45,7 @@ const _getOrCreateSignal = (
/**
* Function to get the signal to indicate if the search has changed.
* It is use by reset-button to know if it should be displayed.
+ * @param searchName
*/
export const canResetSearch = (searchName: string) => {
return _getOrCreateSignal(_canResetSearch, searchName);
@@ -45,7 +54,17 @@ export const canResetSearch = (searchName: string) => {
/**
* Function to get the signal to indicate if the search has changed.
* it is used by the search button to know if it should be displayed.
+ * @param searchName
*/
export const isSearchChanged = (searchName: string) => {
return _getOrCreateSignal(_isSearchChanged, searchName);
};
+
+/**
+ * Function to get the signal to indicate if the search is loading.
+ * The search-results utilize this to determine whether the loading card should be displayed.
+ * @param searchName
+ */
+export const isSearchLoading = (searchName: string) => {
+ return _getOrCreateSignal(_isSearchLoading, searchName);
+};