Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Dev] Test search via API #980

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ NG_USE_Q_AND_AS=true
# Enable Search-feature for 'Q & A's (Possible value: `true` or leave empty for `false`)
NG_USE_Q_AND_A_SEARCH=true

# (Optional) Server-side Search-feature via API (Requires NG_USE_Q_AND_A_SEARCH to be true!)
SEARCH_API=
SEARCH_API_KEY=

# Enable Feedback-prompt on Sub-Category and Offer pages (Possible value: `true` or leave empty for `false`)
NG_USE_FEEDBACK_PROMPT=true

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export class FeedbackLinkComponent implements OnChanges, OnInit {
LoggingEventCategory.ai,
LoggingEvent.FeedbackAnswered,
{
name: value, // Use "name"-property for Matomo
answer: value,
},
);
Expand Down
3 changes: 1 addition & 2 deletions src/app/components/offer/offer.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { NgFor, NgIf } from '@angular/common';
import { Component, Input } from '@angular/core';
import { RouterLink } from '@angular/router';
import { IonImg } from '@ionic/angular/standalone';
import { MarkdownModule } from 'ngx-markdown';
import {
Expand All @@ -17,7 +16,7 @@ import { formatPhoneNumberAsUrl } from 'src/app/shared/utils';
templateUrl: './offer.component.html',
styleUrls: ['./offer.component.scss'],
standalone: true,
imports: [MarkdownModule, IonImg, NgIf, NgFor, RouterLink],
imports: [MarkdownModule, IonImg, NgIf, NgFor],
})
export class OfferComponent {
@Input()
Expand Down
1 change: 1 addition & 0 deletions src/app/components/q-a-set/q-a-set.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export class QASetComponent {
? LoggingEvent.QuestionOpen
: LoggingEvent.QuestionClose,
{
name: slug, // Use "name"-property for Matomo
questionSlug: slug,
question: question.substring(0, 100),
},
Expand Down
29 changes: 21 additions & 8 deletions src/app/components/search-input/search-input.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,31 @@
role="search"
#ngForm
(ngSubmit)="doSubmit()"
class="search-input"
>
<input
type="search"
name="q"
[(ngModel)]="searchQuery"
enterkeyhint="search"
class="input-field--text text-style--size-1"
/>
<span class="search-input--input">
<input
type="search"
name="q"
[(ngModel)]="searchQuery"
enterkeyhint="search"
class="input-field--text text-style--size-1"
/>

<button
*ngIf="!!searchQuery"
type="button"
class="input-field--clear"
[title]="clearLabel || 'Clear'"
(click)="searchQuery = ''; doSubmit(); ngForm.q.focus()"
>
<strong>&times;</strong>
</button>
</span>

<button
type="submit"
class="action is_round is_inline is_outline text-style--alt text-style--size-1"
class="search-input--action action is_round is_inline is_outline text-style--alt text-style--size-1"
>
<strong>
{{ actionLabel || 'Search' }}
Expand Down
38 changes: 30 additions & 8 deletions src/app/components/search-input/search-input.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,34 @@
--search-input--focus: var(--hia_level_text, black);
}

form {
.search-input {
padding-block-start: 1em;
padding-block-end: 1em;

// Layout
display: flex;
gap: 0.5em;

input[type='search'] {
.search-input--input {
flex: 1 1 80%;
position: relative;
}
button[type='submit'] {
flex: 0 0 auto;
.search-input--action {
flex: 0 0 20%;
word-wrap: normal;
}
}

.input-field--clear,
.input-field--text {
border: 0;
border-radius: 0.25em;

&:focus {
outline-color: var(--search-input--focus);
outline-offset: 0.125rem;
outline-width: 0.125rem;
outline-style: solid;
}
}

Expand All @@ -30,11 +45,18 @@ form {
border-radius: 0.25em;
padding-block: 0.25em;
padding-inline: 0.5em;
width: 100%;
}

.input-field--clear {
position: absolute;
inset-block-start: 0.25rem;
inset-inline-end: 0.25rem;
display: inline-block;
padding: 0.25rem;
background: transparent;

&:focus {
outline-color: var(--search-input--focus);
outline-offset: 2px;
outline-width: 2px;
outline-style: solid;
outline-color: gold;
}
}
6 changes: 5 additions & 1 deletion src/app/components/search-input/search-input.component.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { NgIf } from '@angular/common';
import type { OnInit } from '@angular/core';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormsModule } from '@angular/forms';
Expand All @@ -7,7 +8,7 @@ import { FormsModule } from '@angular/forms';
templateUrl: './search-input.component.html',
styleUrls: ['./search-input.component.scss'],
standalone: true,
imports: [FormsModule],
imports: [FormsModule, NgIf],
})
export class SearchInputComponent implements OnInit {
@Input()
Expand All @@ -22,6 +23,9 @@ export class SearchInputComponent implements OnInit {
@Input()
public actionLabel: string;

@Input()
public clearLabel: string;

private previousQuery: string;

constructor() {}
Expand Down
41 changes: 36 additions & 5 deletions src/app/pages/search/search.page.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
<app-parent-link [region]="region"></app-parent-link>

<h2 class="text-style--alt text-style--size-2">
{{ regionData?.labelSearchPageTitle }}
<ng-container *ngIf="!useSearchApi">
{{ regionData?.labelSearchPageTitle }}
</ng-container>
<ng-container *ngIf="useSearchApi"> What are you looking for? </ng-container>
</h2>

<p *ngIf="useSearchApi">Search by typing a question or relevant keywords.</p>

<app-search-input
[(searchQuery)]="searchQuery"
(doSearch)="performSearch($event)"
(doSearch)="onSearchInput($event)"
[actionLabel]="regionData?.labelSearchAction"
class="ion-margin-vertical"
></app-search-input>
Expand All @@ -17,12 +22,38 @@ <h2 class="text-style--alt text-style--size-2">
[tabindex]="-1"
class="focus--minimal focus--fade-out"
>
<p *ngIf="!!searchQuery">
{{ regionData?.labelSearchResultsCount }}
<strong>{{ searchResults?.length }}</strong>
<div
*ngIf="useSearchApi"
class="ion-padding"
style="border: 1px solid white; border-radius: 0.25em"
>
<div *ngIf="loadingSearch">
<span
class="loading-text"
style="width: 95%"
></span>
<span class="loading-text"></span>
<span
class="loading-text"
style="width: 88%"
></span>
<span
class="loading-text"
style="width: 75%"
></span>
</div>
</div>

<p
*ngIf="
useSearchApi && !!searchQuery && !loadingSearch && searchResults?.length
"
>
For more detailed information, check out these FAQs:
</p>

<app-q-a-set-list
*ngIf="!!searchQuery && !loadingSearch"
[baseUrl]="'../'"
[list]="searchResults"
[showDateUpdatedOutsideQuestion]="false"
Expand Down
89 changes: 84 additions & 5 deletions src/app/pages/search/search.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,52 @@ import { NgIf } from '@angular/common';
import type { OnInit } from '@angular/core';
import { Component } from '@angular/core';
import type { Params } from '@angular/router';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
import { ParentLinkComponent } from 'src/app/components/parent-link/parent-link.component';
import { QASetListComponent } from 'src/app/components/q-a-set-list/q-a-set-list.component';
import { SearchInputComponent } from 'src/app/components/search-input/search-input.component';
import type { QASet } from 'src/app/models/qa-set.model';
import type { RegionData } from 'src/app/models/region-data';
import { ConfigService } from 'src/app/services/config.service';
import { OffersService } from 'src/app/services/offers.service';
import { PageMetaService } from 'src/app/services/page-meta.service';
import { RegionDataService } from 'src/app/services/region-data.service';
import { SearchService } from 'src/app/services/search.service';
import { environment } from 'src/environments/environment';
import { AppPath } from 'src/routes';

type SearchApiResponse = {
status?: number;
references?: {
category: string;
subcategory: string;
slug?: string;
parent?: string;
}[];
};

@Component({
selector: 'app-search-page',
templateUrl: './search.page.html',
styleUrls: ['./search.page.css'],
standalone: true,
imports: [
NgIf,
RouterLink,
QASetListComponent,
SearchInputComponent,
ParentLinkComponent,
],
})
export default class SearchPageComponent implements OnInit {
public useSearchApi = environment.useQandASearch && !!environment.searchApi;

public region: string;
public regionData: RegionData;
public qaSets: QASet[];

public searchQuery: string;
public searchResults: QASet[];
public loadingSearch: boolean;

constructor(
private route: ActivatedRoute,
Expand All @@ -41,6 +56,7 @@ export default class SearchPageComponent implements OnInit {
private offersService: OffersService,
private searchService: SearchService,
private pageMeta: PageMetaService,
private configService: ConfigService,
) {}

async ngOnInit() {
Expand Down Expand Up @@ -89,15 +105,34 @@ export default class SearchPageComponent implements OnInit {
}
}

public performSearch(rawQuery: string): void {
const safeQuery = this.searchService.sanitizeSearchQuery(rawQuery);
public onSearchInput(rawQuery: string) {
const safeQuery = this.useSearchApi
? rawQuery
: this.searchService.sanitizeSearchQuery(rawQuery);

this.router.navigate([], {
queryParams: { q: safeQuery },
queryParamsHandling: 'merge',
});
}

public async performSearch(query: string): Promise<void> {
const safeQuery = this.searchService.sanitizeSearchQuery(query);

this.searchResults = this.searchService.query(safeQuery);
if (this.useSearchApi) {
this.loadingSearch = true;
const apiResponse: SearchApiResponse =
await this.fetchApiResults(safeQuery);

if (apiResponse) {
this.loadingSearch = false;
}
if (apiResponse && apiResponse.references) {
this.searchResults = this.createReferences(apiResponse.references);
}
} else {
this.searchResults = this.searchService.query(safeQuery);
}

if (this.searchResults.length > 1) {
this.pageMeta.setTitle({
Expand All @@ -111,4 +146,48 @@ export default class SearchPageComponent implements OnInit {
}
}
}

private createReferences(
references: SearchApiResponse['references'],
): QASet[] {
const results = references.map((reference) => {
const result = this.qaSets.find((qa) => {
return (
qa.categoryID === Number(reference.category) &&
qa.subCategoryID === Number(reference.subcategory)
);
});

return result;
});
return results;
}

private async fetchApiResults(query: string): Promise<SearchApiResponse> {
const response = await window.fetch(environment.searchApi, {
method: 'POST',
credentials: 'omit',
mode: 'cors',
headers: {
Authorization: environment.searchApiKey,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
question: query,
googleSheetId: this.configService.getRegionByRegionSlug(this.region)
.sheetId,
locale: this.regionData.localeLanguage,
}),
});

if (!response || !response.ok) {
console.warn('Something went wrong:', response);
return {
references: [],
};
}

return await response.json();
}
}
3 changes: 1 addition & 2 deletions src/app/referral/referral.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
RouterLink,
RouterOutlet,
} from '@angular/router';
import { IonContent, IonFooter, IonHeader } from '@ionic/angular/standalone';
import { IonContent, IonHeader } from '@ionic/angular/standalone';
import { MarkdownComponent } from 'ngx-markdown';
import { filter } from 'rxjs';
import { AppHeaderComponent } from 'src/app/components/header/header.component';
Expand Down Expand Up @@ -46,7 +46,6 @@ import { AppPath } from 'src/routes';
RouterOutlet,
IonHeader,
IonContent,
IonFooter,
],
})
export class ReferralPageComponent implements OnInit {
Expand Down
Loading