src/app/pages/research-page/research-page.component.ts
Research page with filtering, sorting, and dual view modes. Displays research outputs with multi-faceted filtering and query parameter sync.
| changeDetection | ChangeDetectionStrategy.OnPush |
| providers |
ResearchStore
|
| selector | cns-research-page |
| imports |
HraCommonModule
ButtonsModule
CardsModule
EndOfResultsIndicatorComponent
FilterMenuComponent
FooterComponent
GalleryGridComponent
GalleryGridItemDirective
IconsModule
ListViewComponent
MatDivider
MatFormFieldModule
MatSelectModule
MatSidenavModule
NoResultsIndicatorComponent
SearchFilterComponent
SectionLinkComponent
ScrollingModule
|
| templateUrl | ./research-page.component.html |
| styleUrl | ./research-page.component.scss |
Properties |
|
Methods |
Inputs |
constructor()
|
|
Initializes store with data and registers sidebar |
| events | |
Type : ResearchData
|
|
| Required : true | |
|
Events research data |
|
| eventTypes | |
Type : ResearchTypesData
|
|
| Required : true | |
|
Event type definitions |
|
| funding | |
Type : ResearchData
|
|
| Required : true | |
|
Funding research data |
|
| fundingTypes | |
Type : ResearchTypesData
|
|
| Required : true | |
|
Funding type definitions |
|
| news | |
Type : ResearchData
|
|
| Required : true | |
|
News research data |
|
| people | |
Type : PeopleData
|
|
| Required : true | |
|
People data for filtering and display |
|
| publications | |
Type : ResearchData
|
|
| Required : true | |
|
Publications research data |
|
| publicationTypes | |
Type : ResearchTypesData
|
|
| Required : true | |
|
Publication type definitions |
|
| tags | |
Type : TagsData
|
|
| Required : true | |
|
Tags data from resolver |
|
| visualizations | |
Type : ResearchData
|
|
| Required : true | |
|
Visualizations research data |
|
| getTagItems | ||||||||
getTagItems(ids: TagId[])
|
||||||||
|
Gets tag items from array of tag ids using the store's tags map
Parameters :
Returns :
TagItem[]
tag items |
| Protected Readonly getImageUrl |
Type : unknown
|
Default value : getImageUrl
|
|
Utility to get image URL for a research item |
| Protected Readonly sidebarStore |
Type : unknown
|
Default value : inject(SidebarStore)
|
|
Sidebar store for managing sidebar visibility |
| Protected Readonly store |
Type : unknown
|
Default value : inject(ResearchStore)
|
|
Research store for state management |
import { ChangeDetectionStrategy, Component, computed, effect, inject, input, viewChild } from '@angular/core';
import { MatDivider } from '@angular/material/divider';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { MatSidenav, MatSidenavModule } from '@angular/material/sidenav';
import { HraCommonModule } from '@hra-ui/common';
import { ButtonsModule } from '@hra-ui/design-system/buttons';
import { CardsModule } from '@hra-ui/design-system/cards';
import { TagItem } from '@hra-ui/design-system/cards/gallery-card';
import { ListViewComponent } from '@hra-ui/design-system/content-templates/list-view';
import { SectionLinkComponent } from '@hra-ui/design-system/content-templates/section-link';
import { FilterMenuComponent } from '@hra-ui/design-system/filter-menu';
import { GalleryGridComponent, GalleryGridItemDirective } from '@hra-ui/design-system/gallery-grid';
import { IconsModule } from '@hra-ui/design-system/icons';
import { EndOfResultsIndicatorComponent } from '@hra-ui/design-system/indicators/end-of-results';
import { NoResultsIndicatorComponent } from '@hra-ui/design-system/indicators/no-results-indicator';
import { ScrollingModule } from '@hra-ui/design-system/scrolling';
import { SearchFilterComponent } from '@hra-ui/design-system/search-filter';
import { FooterComponent } from '../../components/footer/footer.component';
import { PeopleData } from '../../schemas/people.schema';
import { ResearchTypesData } from '../../schemas/research-type.schema';
import { ResearchData } from '../../schemas/research.schema';
import { TagId, TagsData } from '../../schemas/tags.schema';
import { SidebarStore } from '../../state/sidebar/sidebar.store';
import { getImageUrl } from '../../utils/research-item-images';
import { ResearchStore } from './state/research.store';
/**
* Research page with filtering, sorting, and dual view modes.
* Displays research outputs with multi-faceted filtering and query parameter sync.
*/
@Component({
selector: 'cns-research-page',
imports: [
HraCommonModule,
ButtonsModule,
CardsModule,
EndOfResultsIndicatorComponent,
FilterMenuComponent,
FooterComponent,
GalleryGridComponent,
GalleryGridItemDirective,
IconsModule,
ListViewComponent,
MatDivider,
MatFormFieldModule,
MatSelectModule,
MatSidenavModule,
NoResultsIndicatorComponent,
SearchFilterComponent,
SectionLinkComponent,
ScrollingModule,
],
templateUrl: './research-page.component.html',
styleUrl: './research-page.component.scss',
providers: [ResearchStore],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ResearchPageComponent {
/** News research data */
readonly news = input.required<ResearchData>();
/** Publications research data */
readonly publications = input.required<ResearchData>();
/** Events research data */
readonly events = input.required<ResearchData>();
/** Funding research data */
readonly funding = input.required<ResearchData>();
/** Visualizations research data */
readonly visualizations = input.required<ResearchData>();
/** People data for filtering and display */
readonly people = input.required<PeopleData>();
/** Publication type definitions */
readonly publicationTypes = input.required<ResearchTypesData>();
/** Event type definitions */
readonly eventTypes = input.required<ResearchTypesData>();
/** Funding type definitions */
readonly fundingTypes = input.required<ResearchTypesData>();
/** Tags data from resolver */
readonly tags = input.required<TagsData>();
/** Research store for state management */
protected readonly store = inject(ResearchStore);
/** Sidebar store for managing sidebar visibility */
protected readonly sidebarStore = inject(SidebarStore);
/** Sidebar component reference */
private readonly sidebar = viewChild.required(MatSidenav);
/** Combined research items from news, publications, events, funding, and visualizations */
private readonly researchItems = computed(() => [
...this.news(),
...this.publications(),
...this.events(),
...this.funding(),
...this.visualizations(),
]);
/** Utility to get image URL for a research item */
protected readonly getImageUrl = getImageUrl;
/** Initializes store with data and registers sidebar */
constructor() {
this.store.setResearchItems(this.researchItems);
this.store.setPeopleItems(this.people);
this.store.setPublicationTypes(this.publicationTypes);
this.store.setEventTypes(this.eventTypes);
this.store.setFundingTypes(this.fundingTypes);
this.store.setTags(this.tags);
effect((onCleanup) => {
this.sidebarStore.setSidebar(this.sidebar());
onCleanup(() => this.sidebarStore.clearSidebar());
});
}
/**
* Gets tag items from array of tag ids using the store's tags map
* @param ids tag ids
* @returns tag items
*/
getTagItems(ids: TagId[]): TagItem[] {
return ids.map((id) => {
const tag = this.store.tagsMap().get(id);
return tag ? { slug: id, name: tag.name, description: tag.description } : { slug: id, name: id, description: '' };
});
}
}
<mat-sidenav-container class="container">
<mat-sidenav
class="sidebar"
aria-label="Filter research"
[mode]="sidebarStore.mode()"
[opened]="sidebarStore.isOpen()"
[disableClose]="sidebarStore.mode() === 'side'"
(closedStart)="sidebarStore.close()"
>
<hra-filter-menu
tagline="Research at the Cyberinfrastructure for Network Science Center"
[filters]="store.filters()"
[counts]="store.counts()"
(filtersChange)="store.updateFilters($event)"
>
<mat-form-field subscriptSizing="dynamic">
<mat-icon class="select-icon" matPrefix>{{ store.view() === 'gallery' ? 'cards' : 'list' }}</mat-icon>
<mat-label>View as</mat-label>
<mat-select [value]="store.view()" (valueChange)="store.setView($event)">
<mat-option value="gallery">
<mat-icon class="select-icon" fontIcon="cards" />
Gallery
</mat-option>
<mat-option value="list">
<mat-icon class="select-icon" fontIcon="list" />
List
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field subscriptSizing="dynamic">
<mat-icon class="select-icon" matPrefix>sort</mat-icon>
<mat-label>Sort by</mat-label>
<mat-select [value]="store.sortBy()" (valueChange)="store.setSortBy($event)">
<mat-option value="nameAsc">Ascending (A-Z) by title</mat-option>
<mat-option value="nameDesc">Descending (Z-A) by title</mat-option>
<mat-option value="newest">Newest</mat-option>
<mat-option value="oldest">Oldest</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field subscriptSizing="dynamic">
<mat-icon class="select-icon" matPrefix>category</mat-icon>
<mat-label>Group by</mat-label>
<mat-select [value]="store.groupBy()" (valueChange)="store.setGroupBy($event)">
<mat-option [value]="null">None</mat-option>
<mat-option value="publicationType">Publication type</mat-option>
<mat-option value="year">Year</mat-option>
</mat-select>
</mat-form-field>
</hra-filter-menu>
</mat-sidenav>
<mat-sidenav-content role="main">
<hra-search-filter
label="Search"
separator="/"
[totalCount]="store.numResearchItems()"
[viewingCount]="store.numFilteredItems()"
[search]="store.search() ?? ''"
(searchChange)="store.setSearch($event)"
/>
<ng-scrollbar hraScrollOverflowFade>
<div class="content">
@if (store.numFilteredItems() === 0) {
<hra-no-results-indicator (clearFilters)="store.resetFilters()" />
} @else if (store.view() === 'gallery') {
@for (group of store.sortedGroupedItems(); track group.label) {
<section class="gallery-group">
@if (group.label) {
<h3 hra-section-link underlined class="title">{{ group.label }}</h3>
}
<hra-gallery-grid [dataSource]="group.items">
<hra-gallery-card
*hraGalleryGridItem="let item"
[imageSrc]="getImageUrl(item)"
[tagline]="item.title"
[date]="(item.dateStart | date) ?? ''"
[link]="item.link || ''"
[external]="true"
[tags]="getTagItems(item.tags)"
/>
</hra-gallery-grid>
</section>
}
} @else {
<hra-list-view [data]="store.sortedGroupedListItems()" [groupBy]="store.groupBy() !== null" />
}
<div class="page-end">
<mat-divider />
<hra-end-of-results-indicator [count]="store.numFilteredItems()" />
</div>
<cns-footer />
</div>
</ng-scrollbar>
</mat-sidenav-content>
</mat-sidenav-container>