From c6c7bd2713a389ac7d0d3c56cd8538a4a5d5ae4c Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Tue, 9 Sep 2025 12:18:11 -0400 Subject: [PATCH 1/5] feat(banner): Add services and models for scheduled banners --- src/app/shared/mappers/banner.mapper.ts | 18 +++++++++ src/app/shared/models/banners.model.ts | 32 +++++++++++++++ src/app/shared/services/banners.service.ts | 38 ++++++++++++++++++ .../shared/stores/banners/banners.actions.ts | 3 ++ .../shared/stores/banners/banners.model.ts | 6 +++ .../stores/banners/banners.selectors.ts | 11 +++++ .../shared/stores/banners/banners.state.ts | 40 +++++++++++++++++++ src/app/shared/stores/banners/index.ts | 4 ++ src/environments/environment.local.ts | 1 + src/environments/environment.test-osf.ts | 1 + src/environments/environment.ts | 4 ++ 11 files changed, 158 insertions(+) create mode 100644 src/app/shared/mappers/banner.mapper.ts create mode 100644 src/app/shared/models/banners.model.ts create mode 100644 src/app/shared/services/banners.service.ts create mode 100644 src/app/shared/stores/banners/banners.actions.ts create mode 100644 src/app/shared/stores/banners/banners.model.ts create mode 100644 src/app/shared/stores/banners/banners.selectors.ts create mode 100644 src/app/shared/stores/banners/banners.state.ts create mode 100644 src/app/shared/stores/banners/index.ts diff --git a/src/app/shared/mappers/banner.mapper.ts b/src/app/shared/mappers/banner.mapper.ts new file mode 100644 index 000000000..cadb824ed --- /dev/null +++ b/src/app/shared/mappers/banner.mapper.ts @@ -0,0 +1,18 @@ +import { Banner, BannerJsonApi } from '../models/banners.model'; + +export class BannerMapper { + static fromResponse(response: BannerJsonApi): Banner { + return { + id: response.data.id, + startDate: new Date(response.data.attributes.start_date), + endDate: new Date(response.data.attributes.end_date), + color: response.data.attributes.color, + license: response.data.attributes.license, + name: response.data.attributes.name, + defaultAltText: response.data.attributes.default_alt_text, + mobileAltText: response.data.attributes.mobile_alt_text, + defaultPhoto: response.data.links.default_photo, + mobilePhoto: response.data.links.mobile_photo, + }; + } +} diff --git a/src/app/shared/models/banners.model.ts b/src/app/shared/models/banners.model.ts new file mode 100644 index 000000000..4bb9ac398 --- /dev/null +++ b/src/app/shared/models/banners.model.ts @@ -0,0 +1,32 @@ +export interface Banner { + id: string; + startDate: Date; + endDate: Date; + color: string; + license: string; + name: string; + defaultAltText: string; + mobileAltText: string; + defaultPhoto: string; + mobilePhoto: string; +} + +export interface BannerJsonApi { + data: { + id: string; + attributes: { + start_date: string; + end_date: string; + color: string; + license: string; + name: string; + default_alt_text: string; + mobile_alt_text: string; + }; + links: { + default_photo: string; + mobile_photo: string; + }; + type: string; + }; +} diff --git a/src/app/shared/services/banners.service.ts b/src/app/shared/services/banners.service.ts new file mode 100644 index 000000000..45269b5d3 --- /dev/null +++ b/src/app/shared/services/banners.service.ts @@ -0,0 +1,38 @@ +import { map, Observable } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { JsonApiResponse } from '@shared/models'; +import { JsonApiService } from '@shared/services'; + +import { BannerMapper } from '../mappers/banner.mapper'; +import { Banner, BannerJsonApi } from '../models/banners.model'; + +import { environment } from 'src/environments/environment'; + +/** + * Service for fetching scheduled banners from OSF API v2 + */ +@Injectable({ + providedIn: 'root', +}) +export class BannersService { + /** + * Injected instance of the JSON:API service used for making API requests. + * This service handles standardized JSON:API request and response formatting. + */ + private jsonApiService = inject(JsonApiService); + + /** + * Retrieves a list of external storage service addons by type. + * + * @param addonType - The addon type to fetch (e.g., 'storage'). + * @returns Observable emitting an array of mapped Addon objects. + * + */ + fetchCurrentBanner(): Observable { + return this.jsonApiService + .get>(`${environment.apiUrlPrivate}/banners/current`) + .pipe(map((response) => BannerMapper.fromResponse(response.data))); + } +} diff --git a/src/app/shared/stores/banners/banners.actions.ts b/src/app/shared/stores/banners/banners.actions.ts new file mode 100644 index 000000000..2622a7897 --- /dev/null +++ b/src/app/shared/stores/banners/banners.actions.ts @@ -0,0 +1,3 @@ +export class FetchCurrentScheduledBanners { + static readonly type = '[Banners] Get Current Scheduled Banners'; +} diff --git a/src/app/shared/stores/banners/banners.model.ts b/src/app/shared/stores/banners/banners.model.ts new file mode 100644 index 000000000..97df8d10c --- /dev/null +++ b/src/app/shared/stores/banners/banners.model.ts @@ -0,0 +1,6 @@ +import { Banner } from '@osf/shared/models/banners.model'; +import { AsyncStateModel } from '@shared/models/store'; + +export interface BannersStateModel { + currentBanner: AsyncStateModel; +} diff --git a/src/app/shared/stores/banners/banners.selectors.ts b/src/app/shared/stores/banners/banners.selectors.ts new file mode 100644 index 000000000..40b57120f --- /dev/null +++ b/src/app/shared/stores/banners/banners.selectors.ts @@ -0,0 +1,11 @@ +import { Selector } from '@ngxs/store'; + +import { BannersStateModel } from './banners.model'; +import { BannersState } from './banners.state'; + +export class BookmarksSelectors { + @Selector([BannersState]) + static getCurrentBanner(state: BannersStateModel) { + return state.currentBanners; + } +} diff --git a/src/app/shared/stores/banners/banners.state.ts b/src/app/shared/stores/banners/banners.state.ts new file mode 100644 index 000000000..6a9c99a21 --- /dev/null +++ b/src/app/shared/stores/banners/banners.state.ts @@ -0,0 +1,40 @@ +import { Action, State, StateContext } from '@ngxs/store'; + +import { tap } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { BannersService } from '@osf/shared/services/banners.service'; + +import { FetchCurrentScheduledBanners } from './banners.actions'; +import { BannersStateModel } from './banners.model'; + +@State({ + name: 'banners', +}) +@Injectable() +export class BannersState { + bannerService = inject(BannersService); + + @Action(FetchCurrentScheduledBanners) + fetchCurrentScheduledBanner(ctx: StateContext) { + const state = ctx.getState(); + ctx.patchState({ + currentBanner: { + ...state.currentBanner, + isLoading: true, + }, + }); + return this.bannerService.fetchCurrentBanner().pipe( + tap((newValue) => { + ctx.patchState({ + currentBanner: { + data: newValue, + isLoading: false, + error: null, + }, + }); + }) + ); + } +} diff --git a/src/app/shared/stores/banners/index.ts b/src/app/shared/stores/banners/index.ts new file mode 100644 index 000000000..663fcdd14 --- /dev/null +++ b/src/app/shared/stores/banners/index.ts @@ -0,0 +1,4 @@ +export * from './banners.actions'; +export * from './banners.model'; +export * from './banners.selectors'; +export * from './banners.state'; diff --git a/src/environments/environment.local.ts b/src/environments/environment.local.ts index 31c69529e..00051a3ca 100644 --- a/src/environments/environment.local.ts +++ b/src/environments/environment.local.ts @@ -3,6 +3,7 @@ export const environment = { webUrl: 'http://localhost:5000', downloadUrl: 'http://localhost:5000/download', apiUrl: 'http://localhost:8000/v2', + apiUrlPrivate: 'http://localhost:8000/_', apiUrlV1: 'http://localhost:5000/api/v1', apiDomainUrl: 'http://localhost:8000', shareDomainUrl: 'https://localhost:8003/trove', diff --git a/src/environments/environment.test-osf.ts b/src/environments/environment.test-osf.ts index 47efc1648..bf6b891be 100644 --- a/src/environments/environment.test-osf.ts +++ b/src/environments/environment.test-osf.ts @@ -6,6 +6,7 @@ export const environment = { webUrl: 'https://test.osf.io/', downloadUrl: 'https://test.osf.io/download', apiUrl: 'https://api.test.osf.io/v2', + apiUrlPrivate: 'http://api.test.osf.io/_', apiUrlV1: 'https://test.osf.io/api/v1', apiDomainUrl: 'https://api.test.osf.io', shareDomainUrl: 'https://test-share.osf.io/trove', diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 504cc1b5c..dc26d5c6c 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -22,6 +22,10 @@ export const environment = { * Base URL for the OSF JSON:API v2 endpoints. */ apiUrl: 'https://api.staging4.osf.io/v2', + /** + * Base URL for non-public OSF JSONLAPI v2 endpoints + */ + apiUrlPrivate: 'https://api.staging4.osf.io/_', /** * Legacy v1 API endpoint used by some older services. */ From f3fe12f8dd07a48cda8aaa67ff08a8200595a6c2 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Thu, 11 Sep 2025 22:58:19 -0400 Subject: [PATCH 2/5] feat(osf): Add scheduled banner --- .../core/constants/ngxs-states.constant.ts | 2 ++ .../pages/dashboard/dashboard.component.html | 2 +- .../pages/dashboard/dashboard.component.ts | 11 ++++++- .../institutions-list.component.html | 2 +- .../institutions-list.component.ts | 2 ++ .../registries-landing.component.html | 2 +- .../registries-landing.component.ts | 2 ++ .../schedule-banner.component.spec.ts | 0 .../scheduled-banner.component.html | 19 +++++++++++ .../scheduled-banner.component.scss | 17 ++++++++++ .../scheduled-banner.component.ts | 30 +++++++++++++++++ src/app/shared/mappers/banner.mapper.ts | 21 ++++++------ src/app/shared/models/banners.model.ts | 32 +++++++++---------- src/app/shared/services/banners.service.ts | 5 ++- .../shared/stores/banners/banners.actions.ts | 4 +-- .../stores/banners/banners.selectors.ts | 4 +-- .../shared/stores/banners/banners.state.ts | 8 ++--- src/environments/environment.development.ts | 4 +++ 18 files changed, 126 insertions(+), 41 deletions(-) create mode 100644 src/app/shared/components/scheduled-banner/schedule-banner.component.spec.ts create mode 100644 src/app/shared/components/scheduled-banner/scheduled-banner.component.html create mode 100644 src/app/shared/components/scheduled-banner/scheduled-banner.component.scss create mode 100644 src/app/shared/components/scheduled-banner/scheduled-banner.component.ts diff --git a/src/app/core/constants/ngxs-states.constant.ts b/src/app/core/constants/ngxs-states.constant.ts index fec700212..10337da01 100644 --- a/src/app/core/constants/ngxs-states.constant.ts +++ b/src/app/core/constants/ngxs-states.constant.ts @@ -6,6 +6,7 @@ import { MetadataState } from '@osf/features/metadata/store'; import { ProjectOverviewState } from '@osf/features/project/overview/store'; import { RegistrationsState } from '@osf/features/project/registrations/store'; import { AddonsState, CurrentResourceState, WikiState } from '@osf/shared/stores'; +import { BannersState } from '@osf/shared/stores/banners'; import { GlobalSearchState } from '@shared/stores/global-search'; import { InstitutionsState } from '@shared/stores/institutions'; import { LicensesState } from '@shared/stores/licenses'; @@ -28,4 +29,5 @@ export const STATES = [ MetadataState, CurrentResourceState, GlobalSearchState, + BannersState, ]; diff --git a/src/app/features/home/pages/dashboard/dashboard.component.html b/src/app/features/home/pages/dashboard/dashboard.component.html index 738974891..96f96d2d6 100644 --- a/src/app/features/home/pages/dashboard/dashboard.component.html +++ b/src/app/features/home/pages/dashboard/dashboard.component.html @@ -6,7 +6,7 @@ [buttonLabel]="'home.loggedIn.dashboard.createProject' | translate" (buttonClick)="createProject()" /> - +

{{ 'home.loggedIn.dashboard.quickSearch.goTo' | translate }} diff --git a/src/app/features/home/pages/dashboard/dashboard.component.ts b/src/app/features/home/pages/dashboard/dashboard.component.ts index 989fc28a9..2e29cb4eb 100644 --- a/src/app/features/home/pages/dashboard/dashboard.component.ts +++ b/src/app/features/home/pages/dashboard/dashboard.component.ts @@ -16,6 +16,7 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { CreateProjectDialogComponent } from '@osf/features/my-projects/components'; import { IconComponent, MyProjectsTableComponent, SubHeaderComponent } from '@osf/shared/components'; +import { ScheduledBannerComponent } from '@osf/shared/components/scheduled-banner/scheduled-banner.component'; import { MY_PROJECTS_TABLE_PARAMS } from '@osf/shared/constants'; import { SortOrder } from '@osf/shared/enums'; import { IS_MEDIUM } from '@osf/shared/helpers'; @@ -24,7 +25,15 @@ import { ClearMyResources, GetMyProjects, MyResourcesSelectors } from '@osf/shar @Component({ selector: 'osf-dashboard', - imports: [RouterLink, Button, SubHeaderComponent, MyProjectsTableComponent, IconComponent, TranslatePipe], + imports: [ + RouterLink, + Button, + SubHeaderComponent, + MyProjectsTableComponent, + IconComponent, + TranslatePipe, + ScheduledBannerComponent, + ], templateUrl: './dashboard.component.html', styleUrl: './dashboard.component.scss', providers: [DialogService], diff --git a/src/app/features/institutions/pages/institutions-list/institutions-list.component.html b/src/app/features/institutions/pages/institutions-list/institutions-list.component.html index 8e6ce79b2..b0835baa3 100644 --- a/src/app/features/institutions/pages/institutions-list/institutions-list.component.html +++ b/src/app/features/institutions/pages/institutions-list/institutions-list.component.html @@ -5,7 +5,7 @@ [title]="'institutions.title' | translate" [icon]="'custom-icon-institutions-dark'" /> - +

diff --git a/src/app/features/institutions/pages/institutions-list/institutions-list.component.ts b/src/app/features/institutions/pages/institutions-list/institutions-list.component.ts index 4c9b1516c..75e2c5abe 100644 --- a/src/app/features/institutions/pages/institutions-list/institutions-list.component.ts +++ b/src/app/features/institutions/pages/institutions-list/institutions-list.component.ts @@ -27,6 +27,7 @@ import { SearchInputComponent, SubHeaderComponent, } from '@osf/shared/components'; +import { ScheduledBannerComponent } from '@osf/shared/components/scheduled-banner/scheduled-banner.component'; import { TABLE_PARAMS } from '@osf/shared/constants'; import { parseQueryFilterParams } from '@osf/shared/helpers'; import { QueryParams } from '@osf/shared/models'; @@ -42,6 +43,7 @@ import { FetchInstitutions, InstitutionsSelectors } from '@osf/shared/stores/ins CustomPaginatorComponent, LoadingSpinnerComponent, RouterLink, + ScheduledBannerComponent, ], templateUrl: './institutions-list.component.html', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/features/registries/pages/registries-landing/registries-landing.component.html b/src/app/features/registries/pages/registries-landing/registries-landing.component.html index c3e8bbcde..7b69b503a 100644 --- a/src/app/features/registries/pages/registries-landing/registries-landing.component.html +++ b/src/app/features/registries/pages/registries-landing/registries-landing.component.html @@ -8,7 +8,7 @@ [buttonLabel]="'registries.addRegistration' | translate" (buttonClick)="goToCreateRegistration()" /> - + + + @if (this.isMobile()) { + {{ this.currentBanner().data.mobileAltText }} + } @else { + {{ this.currentBanner().data.defaultAltText }} + } + +
+} diff --git a/src/app/shared/components/scheduled-banner/scheduled-banner.component.scss b/src/app/shared/components/scheduled-banner/scheduled-banner.component.scss new file mode 100644 index 000000000..d51d7282b --- /dev/null +++ b/src/app/shared/components/scheduled-banner/scheduled-banner.component.scss @@ -0,0 +1,17 @@ +.img-responsive { + display: block; + max-width: 100%; +} + +.scheduled-banner-container { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + flex-direction: row; + + .image { + margin: auto; + max-height: 300px; + } +} diff --git a/src/app/shared/components/scheduled-banner/scheduled-banner.component.ts b/src/app/shared/components/scheduled-banner/scheduled-banner.component.ts new file mode 100644 index 000000000..57da39e8c --- /dev/null +++ b/src/app/shared/components/scheduled-banner/scheduled-banner.component.ts @@ -0,0 +1,30 @@ +import { Store } from '@ngxs/store'; + +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; + +import { IS_XSMALL } from '@osf/shared/helpers'; +import { BannersSelector, FetchCurrentScheduledBanner } from '@osf/shared/stores/banners'; + +@Component({ + selector: 'osf-scheduled-banner', + templateUrl: './scheduled-banner.component.html', + styleUrl: './scheduled-banner.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ScheduledBannerComponent implements OnInit { + private readonly store = inject(Store); + protected currentBanner = this.store.selectSignal(BannersSelector.getCurrentBanner); + protected isMobile = toSignal(inject(IS_XSMALL)); + + ngOnInit() { + this.store.dispatch(new FetchCurrentScheduledBanner()); + } + + shouldShowBanner() { + const bannerStartTime = this.currentBanner().data.startDate; + const bannderEndTime = this.currentBanner().data.endDate; + const currentTime = new Date(); + return bannerStartTime < currentTime && bannderEndTime > currentTime; + } +} diff --git a/src/app/shared/mappers/banner.mapper.ts b/src/app/shared/mappers/banner.mapper.ts index cadb824ed..0abf77d69 100644 --- a/src/app/shared/mappers/banner.mapper.ts +++ b/src/app/shared/mappers/banner.mapper.ts @@ -3,16 +3,17 @@ import { Banner, BannerJsonApi } from '../models/banners.model'; export class BannerMapper { static fromResponse(response: BannerJsonApi): Banner { return { - id: response.data.id, - startDate: new Date(response.data.attributes.start_date), - endDate: new Date(response.data.attributes.end_date), - color: response.data.attributes.color, - license: response.data.attributes.license, - name: response.data.attributes.name, - defaultAltText: response.data.attributes.default_alt_text, - mobileAltText: response.data.attributes.mobile_alt_text, - defaultPhoto: response.data.links.default_photo, - mobilePhoto: response.data.links.mobile_photo, + id: response.id, + startDate: new Date(response.attributes.start_date), + endDate: new Date(response.attributes.end_date), + color: response.attributes.color, + license: response.attributes.license, + name: response.attributes.name, + defaultAltText: response.attributes.default_alt_text, + mobileAltText: response.attributes.mobile_alt_text, + defaultPhoto: response.links.default_photo, + mobilePhoto: response.links.mobile_photo, + link: response.attributes.link, }; } } diff --git a/src/app/shared/models/banners.model.ts b/src/app/shared/models/banners.model.ts index 4bb9ac398..644333e43 100644 --- a/src/app/shared/models/banners.model.ts +++ b/src/app/shared/models/banners.model.ts @@ -9,24 +9,24 @@ export interface Banner { mobileAltText: string; defaultPhoto: string; mobilePhoto: string; + link: string; } export interface BannerJsonApi { - data: { - id: string; - attributes: { - start_date: string; - end_date: string; - color: string; - license: string; - name: string; - default_alt_text: string; - mobile_alt_text: string; - }; - links: { - default_photo: string; - mobile_photo: string; - }; - type: string; + id: string; + attributes: { + start_date: string; + end_date: string; + color: string; + license: string; + name: string; + default_alt_text: string; + mobile_alt_text: string; + link: string; + }; + links: { + default_photo: string; + mobile_photo: string; }; + type: string; } diff --git a/src/app/shared/services/banners.service.ts b/src/app/shared/services/banners.service.ts index 45269b5d3..0dba1fd4e 100644 --- a/src/app/shared/services/banners.service.ts +++ b/src/app/shared/services/banners.service.ts @@ -24,10 +24,9 @@ export class BannersService { private jsonApiService = inject(JsonApiService); /** - * Retrieves a list of external storage service addons by type. + * Retrieves the current banner * - * @param addonType - The addon type to fetch (e.g., 'storage'). - * @returns Observable emitting an array of mapped Addon objects. + * @returns Observable emitting a Banner object. * */ fetchCurrentBanner(): Observable { diff --git a/src/app/shared/stores/banners/banners.actions.ts b/src/app/shared/stores/banners/banners.actions.ts index 2622a7897..f1b1bcbde 100644 --- a/src/app/shared/stores/banners/banners.actions.ts +++ b/src/app/shared/stores/banners/banners.actions.ts @@ -1,3 +1,3 @@ -export class FetchCurrentScheduledBanners { - static readonly type = '[Banners] Get Current Scheduled Banners'; +export class FetchCurrentScheduledBanner { + static readonly type = '[Banners] Get Current Scheduled Banner'; } diff --git a/src/app/shared/stores/banners/banners.selectors.ts b/src/app/shared/stores/banners/banners.selectors.ts index 40b57120f..737fa2e2b 100644 --- a/src/app/shared/stores/banners/banners.selectors.ts +++ b/src/app/shared/stores/banners/banners.selectors.ts @@ -3,9 +3,9 @@ import { Selector } from '@ngxs/store'; import { BannersStateModel } from './banners.model'; import { BannersState } from './banners.state'; -export class BookmarksSelectors { +export class BannersSelector { @Selector([BannersState]) static getCurrentBanner(state: BannersStateModel) { - return state.currentBanners; + return state.currentBanner; } } diff --git a/src/app/shared/stores/banners/banners.state.ts b/src/app/shared/stores/banners/banners.state.ts index 6a9c99a21..af4818c71 100644 --- a/src/app/shared/stores/banners/banners.state.ts +++ b/src/app/shared/stores/banners/banners.state.ts @@ -6,7 +6,7 @@ import { inject, Injectable } from '@angular/core'; import { BannersService } from '@osf/shared/services/banners.service'; -import { FetchCurrentScheduledBanners } from './banners.actions'; +import { FetchCurrentScheduledBanner } from './banners.actions'; import { BannersStateModel } from './banners.model'; @State({ @@ -14,9 +14,9 @@ import { BannersStateModel } from './banners.model'; }) @Injectable() export class BannersState { - bannerService = inject(BannersService); + bannersService = inject(BannersService); - @Action(FetchCurrentScheduledBanners) + @Action(FetchCurrentScheduledBanner) fetchCurrentScheduledBanner(ctx: StateContext) { const state = ctx.getState(); ctx.patchState({ @@ -25,7 +25,7 @@ export class BannersState { isLoading: true, }, }); - return this.bannerService.fetchCurrentBanner().pipe( + return this.bannersService.fetchCurrentBanner().pipe( tap((newValue) => { ctx.patchState({ currentBanner: { diff --git a/src/environments/environment.development.ts b/src/environments/environment.development.ts index 685b90a2c..dc0f84a0d 100644 --- a/src/environments/environment.development.ts +++ b/src/environments/environment.development.ts @@ -26,6 +26,10 @@ export const environment = { * Legacy v1 API endpoint used by some older services. */ apiUrlV1: 'https://staging4.osf.io/api/v1', + /** + * Base URL for non-public OSF JSONLAPI v2 endpoints + */ + apiUrlPrivate: 'https://api.staging4.osf.io/_', /** * Domain URL used for JSON:API v2 services. */ From 6372e538f5d651103d4be5caeb15ef18a46c1576 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Fri, 12 Sep 2025 12:04:49 -0400 Subject: [PATCH 3/5] feat(banner): CR followup --- .../scheduled-banner.component.html | 12 ++++++------ .../scheduled-banner/scheduled-banner.component.ts | 14 +++++++------- src/app/shared/mappers/banner.mapper.ts | 5 +++-- .../{banners.model.ts => banner.json-api.model.ts} | 14 -------------- src/app/shared/models/banner.model.ts | 13 +++++++++++++ src/app/shared/services/banners.service.ts | 5 +++-- src/app/shared/stores/banners/banners.actions.ts | 2 +- src/app/shared/stores/banners/banners.model.ts | 4 ++-- src/app/shared/stores/banners/banners.selectors.ts | 7 ++++++- src/app/shared/stores/banners/banners.state.ts | 4 +++- src/app/shared/stores/index.ts | 1 + 11 files changed, 45 insertions(+), 36 deletions(-) rename src/app/shared/models/{banners.model.ts => banner.json-api.model.ts} (57%) create mode 100644 src/app/shared/models/banner.model.ts diff --git a/src/app/shared/components/scheduled-banner/scheduled-banner.component.html b/src/app/shared/components/scheduled-banner/scheduled-banner.component.html index 10a36ab66..2680b5eec 100644 --- a/src/app/shared/components/scheduled-banner/scheduled-banner.component.html +++ b/src/app/shared/components/scheduled-banner/scheduled-banner.component.html @@ -1,17 +1,17 @@ @if (this.shouldShowBanner()) { -
- +
+ @if (this.isMobile()) { {{ this.currentBanner().data.mobileAltText }} } @else { {{ this.currentBanner().data.defaultAltText }} } diff --git a/src/app/shared/components/scheduled-banner/scheduled-banner.component.ts b/src/app/shared/components/scheduled-banner/scheduled-banner.component.ts index 57da39e8c..fef08d051 100644 --- a/src/app/shared/components/scheduled-banner/scheduled-banner.component.ts +++ b/src/app/shared/components/scheduled-banner/scheduled-banner.component.ts @@ -1,6 +1,6 @@ import { Store } from '@ngxs/store'; -import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, inject, OnInit } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { IS_XSMALL } from '@osf/shared/helpers'; @@ -14,17 +14,17 @@ import { BannersSelector, FetchCurrentScheduledBanner } from '@osf/shared/stores }) export class ScheduledBannerComponent implements OnInit { private readonly store = inject(Store); - protected currentBanner = this.store.selectSignal(BannersSelector.getCurrentBanner); - protected isMobile = toSignal(inject(IS_XSMALL)); + currentBanner = this.store.selectSignal(BannersSelector.getCurrentBanner); + isMobile = toSignal(inject(IS_XSMALL)); ngOnInit() { this.store.dispatch(new FetchCurrentScheduledBanner()); } - shouldShowBanner() { - const bannerStartTime = this.currentBanner().data.startDate; - const bannderEndTime = this.currentBanner().data.endDate; + shouldShowBanner = computed(() => { + const bannerStartTime = this.currentBanner().startDate; + const bannderEndTime = this.currentBanner().endDate; const currentTime = new Date(); return bannerStartTime < currentTime && bannderEndTime > currentTime; - } + }); } diff --git a/src/app/shared/mappers/banner.mapper.ts b/src/app/shared/mappers/banner.mapper.ts index 0abf77d69..8526fa40f 100644 --- a/src/app/shared/mappers/banner.mapper.ts +++ b/src/app/shared/mappers/banner.mapper.ts @@ -1,7 +1,8 @@ -import { Banner, BannerJsonApi } from '../models/banners.model'; +import { BannerJsonApi } from '../models/banner.json-api.model'; +import { BannerModel } from '../models/banner.model'; export class BannerMapper { - static fromResponse(response: BannerJsonApi): Banner { + static fromResponse(response: BannerJsonApi): BannerModel { return { id: response.id, startDate: new Date(response.attributes.start_date), diff --git a/src/app/shared/models/banners.model.ts b/src/app/shared/models/banner.json-api.model.ts similarity index 57% rename from src/app/shared/models/banners.model.ts rename to src/app/shared/models/banner.json-api.model.ts index 644333e43..707c896e3 100644 --- a/src/app/shared/models/banners.model.ts +++ b/src/app/shared/models/banner.json-api.model.ts @@ -1,17 +1,3 @@ -export interface Banner { - id: string; - startDate: Date; - endDate: Date; - color: string; - license: string; - name: string; - defaultAltText: string; - mobileAltText: string; - defaultPhoto: string; - mobilePhoto: string; - link: string; -} - export interface BannerJsonApi { id: string; attributes: { diff --git a/src/app/shared/models/banner.model.ts b/src/app/shared/models/banner.model.ts new file mode 100644 index 000000000..87e7f85d9 --- /dev/null +++ b/src/app/shared/models/banner.model.ts @@ -0,0 +1,13 @@ +export interface BannerModel { + id: string; + startDate: Date; + endDate: Date; + color: string; + license: string; + name: string; + defaultAltText: string; + mobileAltText: string; + defaultPhoto: string; + mobilePhoto: string; + link: string; +} diff --git a/src/app/shared/services/banners.service.ts b/src/app/shared/services/banners.service.ts index f3331f224..b8eccd401 100644 --- a/src/app/shared/services/banners.service.ts +++ b/src/app/shared/services/banners.service.ts @@ -6,7 +6,8 @@ import { JsonApiResponse } from '@shared/models'; import { JsonApiService } from '@shared/services'; import { BannerMapper } from '../mappers/banner.mapper'; -import { Banner, BannerJsonApi } from '../models/banners.model'; +import { BannerJsonApi } from '../models/banner.json-api.model'; +import { BannerModel } from '../models/banner.model'; import { environment } from 'src/environments/environment'; @@ -29,7 +30,7 @@ export class BannersService { * @returns Observable emitting a Banner object. * */ - fetchCurrentBanner(): Observable { + fetchCurrentBanner(): Observable { return this.jsonApiService .get>(`${environment.apiDomainUrl}/_/banners/current`) .pipe(map((response) => BannerMapper.fromResponse(response.data))); diff --git a/src/app/shared/stores/banners/banners.actions.ts b/src/app/shared/stores/banners/banners.actions.ts index f1b1bcbde..34ffb22f2 100644 --- a/src/app/shared/stores/banners/banners.actions.ts +++ b/src/app/shared/stores/banners/banners.actions.ts @@ -1,3 +1,3 @@ export class FetchCurrentScheduledBanner { - static readonly type = '[Banners] Get Current Scheduled Banner'; + static readonly type = '[Banners] Fetch Current Scheduled Banner'; } diff --git a/src/app/shared/stores/banners/banners.model.ts b/src/app/shared/stores/banners/banners.model.ts index 97df8d10c..97e0bf28e 100644 --- a/src/app/shared/stores/banners/banners.model.ts +++ b/src/app/shared/stores/banners/banners.model.ts @@ -1,6 +1,6 @@ -import { Banner } from '@osf/shared/models/banners.model'; +import { BannerModel } from '@osf/shared/models/banner.model'; import { AsyncStateModel } from '@shared/models/store'; export interface BannersStateModel { - currentBanner: AsyncStateModel; + currentBanner: AsyncStateModel; } diff --git a/src/app/shared/stores/banners/banners.selectors.ts b/src/app/shared/stores/banners/banners.selectors.ts index 737fa2e2b..5a7192821 100644 --- a/src/app/shared/stores/banners/banners.selectors.ts +++ b/src/app/shared/stores/banners/banners.selectors.ts @@ -6,6 +6,11 @@ import { BannersState } from './banners.state'; export class BannersSelector { @Selector([BannersState]) static getCurrentBanner(state: BannersStateModel) { - return state.currentBanner; + return state.currentBanner.data; + } + + @Selector([BannersState]) + static getCurrentBannerIsLoading(state: BannersStateModel) { + return state.currentBanner.isLoading; } } diff --git a/src/app/shared/stores/banners/banners.state.ts b/src/app/shared/stores/banners/banners.state.ts index af4818c71..97ae650cd 100644 --- a/src/app/shared/stores/banners/banners.state.ts +++ b/src/app/shared/stores/banners/banners.state.ts @@ -1,9 +1,10 @@ import { Action, State, StateContext } from '@ngxs/store'; -import { tap } from 'rxjs'; +import { catchError, tap } from 'rxjs'; import { inject, Injectable } from '@angular/core'; +import { handleSectionError } from '@osf/shared/helpers'; import { BannersService } from '@osf/shared/services/banners.service'; import { FetchCurrentScheduledBanner } from './banners.actions'; @@ -34,6 +35,7 @@ export class BannersState { error: null, }, }); + catchError((error) => handleSectionError(ctx, 'currentBanner', error)); }) ); } diff --git a/src/app/shared/stores/index.ts b/src/app/shared/stores/index.ts index 7e306561d..bc94b95fa 100644 --- a/src/app/shared/stores/index.ts +++ b/src/app/shared/stores/index.ts @@ -1,4 +1,5 @@ export * from './addons'; +export * from './banners'; export * from './bookmarks'; export * from './citations'; export * from './collections'; From 1f5c516d61a990088eb39969706a801fa00a8dd6 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Fri, 12 Sep 2025 12:21:07 -0400 Subject: [PATCH 4/5] feat(banners): CR followup again --- .../scheduled-banner.component.html | 12 ++++++------ .../scheduled-banner.component.ts | 16 ++++++++++------ src/app/shared/stores/banners/banners.model.ts | 10 +++++++++- src/app/shared/stores/banners/banners.state.ts | 3 ++- 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/app/shared/components/scheduled-banner/scheduled-banner.component.html b/src/app/shared/components/scheduled-banner/scheduled-banner.component.html index 2680b5eec..4673b9bb5 100644 --- a/src/app/shared/components/scheduled-banner/scheduled-banner.component.html +++ b/src/app/shared/components/scheduled-banner/scheduled-banner.component.html @@ -1,17 +1,17 @@ @if (this.shouldShowBanner()) { -
- +
+ @if (this.isMobile()) { } @else { } diff --git a/src/app/shared/components/scheduled-banner/scheduled-banner.component.ts b/src/app/shared/components/scheduled-banner/scheduled-banner.component.ts index fef08d051..83734aa8d 100644 --- a/src/app/shared/components/scheduled-banner/scheduled-banner.component.ts +++ b/src/app/shared/components/scheduled-banner/scheduled-banner.component.ts @@ -1,4 +1,4 @@ -import { Store } from '@ngxs/store'; +import { select, Store } from '@ngxs/store'; import { ChangeDetectionStrategy, Component, computed, inject, OnInit } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; @@ -14,7 +14,7 @@ import { BannersSelector, FetchCurrentScheduledBanner } from '@osf/shared/stores }) export class ScheduledBannerComponent implements OnInit { private readonly store = inject(Store); - currentBanner = this.store.selectSignal(BannersSelector.getCurrentBanner); + currentBanner = select(BannersSelector.getCurrentBanner); isMobile = toSignal(inject(IS_XSMALL)); ngOnInit() { @@ -22,9 +22,13 @@ export class ScheduledBannerComponent implements OnInit { } shouldShowBanner = computed(() => { - const bannerStartTime = this.currentBanner().startDate; - const bannderEndTime = this.currentBanner().endDate; - const currentTime = new Date(); - return bannerStartTime < currentTime && bannderEndTime > currentTime; + const banner = this.currentBanner(); + if (banner) { + const bannerStartTime = banner.startDate; + const bannderEndTime = banner.endDate; + const currentTime = new Date(); + return bannerStartTime < currentTime && bannderEndTime > currentTime; + } + return false; }); } diff --git a/src/app/shared/stores/banners/banners.model.ts b/src/app/shared/stores/banners/banners.model.ts index 97e0bf28e..630578cf2 100644 --- a/src/app/shared/stores/banners/banners.model.ts +++ b/src/app/shared/stores/banners/banners.model.ts @@ -2,5 +2,13 @@ import { BannerModel } from '@osf/shared/models/banner.model'; import { AsyncStateModel } from '@shared/models/store'; export interface BannersStateModel { - currentBanner: AsyncStateModel; + currentBanner: AsyncStateModel; } + +export const BANNERS_DEFAULTS: BannersStateModel = { + currentBanner: { + data: null, + isLoading: false, + error: null, + }, +}; diff --git a/src/app/shared/stores/banners/banners.state.ts b/src/app/shared/stores/banners/banners.state.ts index 97ae650cd..dd153d32f 100644 --- a/src/app/shared/stores/banners/banners.state.ts +++ b/src/app/shared/stores/banners/banners.state.ts @@ -8,10 +8,11 @@ import { handleSectionError } from '@osf/shared/helpers'; import { BannersService } from '@osf/shared/services/banners.service'; import { FetchCurrentScheduledBanner } from './banners.actions'; -import { BannersStateModel } from './banners.model'; +import { BANNERS_DEFAULTS, BannersStateModel } from './banners.model'; @State({ name: 'banners', + defaults: BANNERS_DEFAULTS, }) @Injectable() export class BannersState { From 90fe2e25d62d46a39b5dc223fd1cb91d778e099f Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Fri, 12 Sep 2025 12:28:51 -0400 Subject: [PATCH 5/5] feat(banners): Use primeflex classesl; remove scss file --- .../scheduled-banner.component.html | 9 ++++++--- .../scheduled-banner.component.scss | 17 ----------------- .../scheduled-banner.component.ts | 1 - 3 files changed, 6 insertions(+), 21 deletions(-) delete mode 100644 src/app/shared/components/scheduled-banner/scheduled-banner.component.scss diff --git a/src/app/shared/components/scheduled-banner/scheduled-banner.component.html b/src/app/shared/components/scheduled-banner/scheduled-banner.component.html index 4673b9bb5..d1cac0cdd 100644 --- a/src/app/shared/components/scheduled-banner/scheduled-banner.component.html +++ b/src/app/shared/components/scheduled-banner/scheduled-banner.component.html @@ -1,15 +1,18 @@ @if (this.shouldShowBanner()) { -