Skip to content

Commit 6018a2e

Browse files
committed
ux: user can now toggle product types for search beneath search bar
1 parent 39f296d commit 6018a2e

11 files changed

Lines changed: 543 additions & 52 deletions

angular/src/app/home/home.component.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
}
1818

1919
.section:first-of-type {
20-
margin-top: 2rem;
20+
margin-top: -0.5rem;
2121
}
2222

2323
.section-header {

angular/src/app/search-panel/search-panel.component.html

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
</p-toast>
99

1010
<div>
11-
<div class="content-wrapper p-d-flex p-jc-center" style="margin-top: 1em">
11+
<div class="content-wrapper p-d-flex p-jc-center" style="margin-top: 1em;">
1212
<div>
1313
<h3 *ngIf="title" class="heading title has-text-black">{{ title }}</h3>
1414
<!-- <p-button
@@ -147,7 +147,37 @@ <h3 *ngIf="title" class="heading title has-text-black">{{ title }}</h3>
147147
*ngIf="queryStringError"
148148
[innerHTML]="queryStringErrorMessage"
149149
></div>
150-
150+
151+
<div
152+
class="product-type-row"
153+
*ngIf="visibleProductTypes.length"
154+
aria-label="Toggle product categories"
155+
>
156+
<div class="product-type-label">Products</div>
157+
<div class="product-type-buttons">
158+
<button
159+
type="button"
160+
class="product-type-chip"
161+
*ngFor="let product of visibleProductTypes"
162+
[class.product-type-chip--active]="isProductTypeActive(product.key)"
163+
[class.product-type-chip--disabled]="product.comingSoon"
164+
(click)="onProductTypeToggle(product)"
165+
[attr.aria-pressed]="isProductTypeActive(product.key)"
166+
[disabled]="product.comingSoon"
167+
[attr.title]="product.comingSoon ? 'Coming soon' : null"
168+
>
169+
<i [ngClass]="product.icon" aria-hidden="true"></i>
170+
<span class="product-type-chip__label">{{ product.label }}</span>
171+
<span
172+
*ngIf="product.comingSoon"
173+
class="product-type-chip__tag"
174+
aria-hidden="true"
175+
>soon</span
176+
>
177+
</button>
178+
</div>
179+
</div>
180+
151181
<!--
152182
<div class="p-d-flex p-jc-between" style="margin-top: 1em">
153183
<span

angular/src/app/search-panel/search-panel.component.scss

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ body .p-highlight {
224224
color: white;
225225
width: 100%;
226226
display: inline-block;
227+
margin-bottom: 1rem;
227228
// /* margin: 0em auto 1em; */
228229
}
229230

@@ -262,6 +263,98 @@ body .p-highlight {
262263
0 0 0 1px rgba(139, 229, 255, 0.65);
263264
}
264265

266+
.product-type-row {
267+
width: min(1320px, 96vw);
268+
margin: 0.9rem auto 1rem;
269+
display: flex;
270+
align-items: center;
271+
gap: 0.75rem;
272+
flex-wrap: wrap;
273+
color: rgba(255, 255, 255, 0.85);
274+
}
275+
276+
.product-type-label {
277+
font-size: 0.9rem;
278+
font-weight: 700;
279+
letter-spacing: 0.08em;
280+
text-transform: uppercase;
281+
color: rgba(255, 255, 255, 0.72);
282+
}
283+
284+
.product-type-buttons {
285+
display: flex;
286+
flex-wrap: wrap;
287+
gap: 0.5rem;
288+
}
289+
290+
.product-type-chip {
291+
display: inline-flex;
292+
align-items: center;
293+
gap: 0.45rem;
294+
border: 1.5px solid rgba(255, 255, 255, 0.45);
295+
background: rgba(255, 255, 255, 0.06);
296+
color: #ffffff;
297+
border-radius: 999px;
298+
padding: 0.35rem 0.9rem;
299+
font-weight: 700;
300+
font-size: 0.9rem;
301+
letter-spacing: 0.01em;
302+
backdrop-filter: blur(4px);
303+
transition: all 0.18s ease;
304+
}
305+
306+
.product-type-chip i {
307+
font-size: 0.95rem;
308+
opacity: 0.85;
309+
}
310+
311+
.product-type-chip:hover,
312+
.product-type-chip:focus {
313+
border-color: rgba(255, 255, 255, 0.9);
314+
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.35);
315+
transform: translateY(-1px);
316+
outline: none;
317+
}
318+
319+
.product-type-chip--active {
320+
background: #ffffff;
321+
color: #0f172a;
322+
border-color: #ffffff;
323+
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.32);
324+
}
325+
326+
.product-type-chip--active:hover,
327+
.product-type-chip--active:focus {
328+
color: #0f172a;
329+
background: #ffffff;
330+
border-color: #ffffff;
331+
}
332+
333+
.product-type-chip--active i {
334+
color: #0f172a;
335+
opacity: 1;
336+
}
337+
338+
.product-type-chip--disabled {
339+
opacity: 0.55;
340+
cursor: not-allowed;
341+
border-style: dashed;
342+
}
343+
344+
.product-type-chip__label {
345+
white-space: nowrap;
346+
}
347+
348+
.product-type-chip__tag {
349+
background: rgba(15, 23, 42, 0.9);
350+
color: #ffffff;
351+
font-size: 0.68rem;
352+
letter-spacing: 0.05em;
353+
padding: 0.1rem 0.45rem;
354+
border-radius: 999px;
355+
text-transform: uppercase;
356+
}
357+
265358

266359
.search-input-group .p-inputtext,
267360
.search-input-group .p-autocomplete-input {

angular/src/app/search-panel/search-panel.component.ts

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,27 @@ import {
22
Component,
33
Inject,
44
OnInit,
5+
OnDestroy,
56
Input,
67
NgZone,
78
ViewChild,
89
ElementRef,
9-
Query,
1010
} from "@angular/core";
1111
import { SelectItem } from "primeng/api";
1212
import { DropdownModule } from "primeng/dropdown";
1313
import { TaxonomyListService } from "../shared/taxonomy-list/index";
1414
import { SearchfieldsListService } from "../shared/searchfields-list/index";
15-
import { SearchService, SEARCH_SERVICE } from "../shared/search-service";
15+
import {
16+
SearchService,
17+
SEARCH_SERVICE,
18+
ProductTypeState,
19+
ProductTypeKey,
20+
DEFAULT_PRODUCT_TYPES,
21+
} from "../shared/search-service";
1622
import { Router, NavigationExtras } from "@angular/router";
1723
import * as _ from "lodash-es";
1824
import { AppConfig, Config } from "../shared/config-service/config.service";
19-
import { timer } from "rxjs";
25+
import { Observable, Subscription, timer } from "rxjs";
2026
import {
2127
trigger,
2228
state,
@@ -28,17 +34,24 @@ import { OverlayPanel } from "primeng/overlaypanel";
2834
// MARK: 08/21/2024: Disabled for now until rework
2935
// import { AdvSearchComponent } from "../adv-search/adv-search.component";
3036
import { SearchQueryService } from "../shared/search-query/search-query.service";
31-
import { Observable } from "rxjs";
3237
import {
3338
SDPQuery,
3439
QueryRow,
3540
CurrentQueryInfo,
3641
} from "../shared/search-query/query";
37-
import { ReadVarExpr } from "@angular/compiler";
3842
import { find } from "./autocomplete";
3943
// import fetch from "cross-fetch";
4044
import { MessageService } from "primeng/api";
4145

46+
type ProductTypeOption = {
47+
key: ProductTypeKey;
48+
label: string;
49+
icon: string;
50+
external?: boolean;
51+
comingSoon?: boolean;
52+
description?: string;
53+
};
54+
4255
@Component({
4356
selector: "app-search-panel",
4457
templateUrl: "./search-panel.component.html",
@@ -67,7 +80,7 @@ import { MessageService } from "primeng/api";
6780
]),
6881
],
6982
})
70-
export class SearchPanelComponent implements OnInit {
83+
export class SearchPanelComponent implements OnInit, OnDestroy {
7184
@Input() title: string;
7285
@Input() subtitle: boolean;
7386
@Input() helpicon: boolean = false;
@@ -107,6 +120,27 @@ export class SearchPanelComponent implements OnInit {
107120
searchBottonWith: string = "10%";
108121
breadcrumb_top: string = "6em";
109122
includeExternalProducts: boolean = false;
123+
productTypes: ProductTypeState = { ...DEFAULT_PRODUCT_TYPES };
124+
productTypeOptions: ProductTypeOption[] = [
125+
{ key: "data", label: "Data", icon: "pi pi-database" },
126+
{ key: "code", label: "Code", icon: "pi pi-code", external: true },
127+
{
128+
key: "papers",
129+
label: "Papers",
130+
icon: "pi pi-book",
131+
external: true,
132+
comingSoon: true,
133+
},
134+
{
135+
key: "patents",
136+
label: "Patents",
137+
icon: "pi pi-briefcase",
138+
external: true,
139+
comingSoon: true,
140+
},
141+
];
142+
private productTypesSub?: Subscription;
143+
private externalToggleSub?: Subscription;
110144
externalHelperText: string =
111145
"Toggle to include external products like open-source code (patents & papers coming soon). Results and filters will blend with NIST data.";
112146
externalInfoVisible: boolean = false;
@@ -263,9 +297,17 @@ export class SearchPanelComponent implements OnInit {
263297
this.imageURL = this.SDPAPI + "assets/images/ngi-background.png";
264298
});
265299

266-
this.searchService.watchExternalProducts().subscribe((enabled) => {
267-
this.includeExternalProducts = enabled;
268-
});
300+
this.productTypesSub = this.searchService
301+
.watchProductTypes()
302+
.subscribe((state) => {
303+
this.productTypes = state;
304+
});
305+
306+
this.externalToggleSub = this.searchService
307+
.watchExternalProducts()
308+
.subscribe((enabled) => {
309+
this.includeExternalProducts = enabled;
310+
});
269311

270312
this.getTaxonomySuggestions();
271313
}
@@ -313,6 +355,23 @@ export class SearchPanelComponent implements OnInit {
313355
this.externalInfoVisible = false;
314356
}
315357

358+
get visibleProductTypes(): ProductTypeOption[] {
359+
return this.productTypeOptions.filter(
360+
(option) => this.includeExternalProducts || !option.external
361+
);
362+
}
363+
364+
isProductTypeActive(key: ProductTypeKey): boolean {
365+
return !!this.productTypes && !!this.productTypes[key];
366+
}
367+
368+
onProductTypeToggle(option: ProductTypeOption) {
369+
if (option.comingSoon) return;
370+
if (option.external && !this.includeExternalProducts) return;
371+
const nextState = !this.isProductTypeActive(option.key);
372+
this.searchService.setProductTypeEnabled(option.key, nextState);
373+
}
374+
316375
/**
317376
* Hide syntax rules and field lookup help
318377
*/
@@ -710,4 +769,9 @@ export class SearchPanelComponent implements OnInit {
710769
showDialog() {
711770
this.visible = true;
712771
}
772+
773+
ngOnDestroy(): void {
774+
this.productTypesSub?.unsubscribe();
775+
this.externalToggleSub?.unsubscribe();
776+
}
713777
}

angular/src/app/search/filters/filters.component.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ import { TreeNode } from "primeng/api";
1414
// import { Message } from 'primeng/components/common/api';
1515
import { Message } from "primeng/api";
1616
import { SDPQuery } from "../../shared/search-query/query";
17-
import { SearchService, SEARCH_SERVICE } from "../../shared/search-service";
17+
import {
18+
SearchService,
19+
SEARCH_SERVICE,
20+
ProductTypeState,
21+
} from "../../shared/search-service";
1822
import { SearchQueryService } from "../../shared/search-query/search-query.service";
1923
import {
2024
TaxonomyListService,
@@ -131,6 +135,8 @@ export class FiltersComponent implements OnInit, AfterViewInit, OnDestroy {
131135
private fullFacetCountsSubscription: any = null;
132136
private searchResponseSub: any = null;
133137
private externalToggleSubscription: any = null;
138+
private productTypeSubscription: any = null;
139+
private lastProductTypes: ProductTypeState | null = null;
134140

135141
filterStyle = {
136142
width: "100%",
@@ -242,13 +248,29 @@ export class FiltersComponent implements OnInit, AfterViewInit, OnDestroy {
242248
.subscribe(() => {
243249
this.resetFacetAggregationState();
244250
});
251+
252+
this.productTypeSubscription = this.searchService
253+
.watchProductTypes()
254+
.subscribe((state) => {
255+
if (!state) return;
256+
if (this.lastProductTypes && _.isEqual(this.lastProductTypes, state)) {
257+
return;
258+
}
259+
this.lastProductTypes = state;
260+
this.resetFacetAggregationState();
261+
// Rebuild facet UI from latest results (if available) so filters don't stall when all products were off.
262+
if (this.searchResults) {
263+
this.onSuccess(this.searchResults);
264+
}
265+
});
245266
}
246267

247268
ngOnDestroy(): void {
248269
if(this.searchResponseSub) { try { this.searchResponseSub.unsubscribe(); } catch {} }
249270
if(this.fullFacetCountsSubscription) { try { this.fullFacetCountsSubscription.unsubscribe(); } catch {} }
250271
if(this.filterWatcherSub) { try { this.filterWatcherSub.unsubscribe(); } catch {} }
251272
if(this.externalToggleSubscription) { try { this.externalToggleSubscription.unsubscribe(); } catch {} }
273+
if(this.productTypeSubscription) { try { this.productTypeSubscription.unsubscribe(); } catch {} }
252274
}
253275

254276
toggleMoreOptions() {

0 commit comments

Comments
 (0)