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 b3e602f2e..39f983cb4 100644 --- a/src/app/features/home/pages/dashboard/dashboard.component.html +++ b/src/app/features/home/pages/dashboard/dashboard.component.html @@ -10,6 +10,7 @@ [buttonLabel]="'home.loggedIn.dashboard.createProject' | translate" (buttonClick)="createProject()" /> +

diff --git a/src/app/features/home/pages/dashboard/dashboard.component.ts b/src/app/features/home/pages/dashboard/dashboard.component.ts index 0ca0b649a..2830b423d 100644 --- a/src/app/features/home/pages/dashboard/dashboard.component.ts +++ b/src/app/features/home/pages/dashboard/dashboard.component.ts @@ -19,6 +19,7 @@ import { IconComponent, LoadingSpinnerComponent, MyProjectsTableComponent, + ScheduledBannerComponent, SubHeaderComponent, } from '@osf/shared/components'; import { DEFAULT_TABLE_PARAMS } from '@osf/shared/constants'; @@ -37,6 +38,7 @@ import { ClearMyResources, GetMyProjects, MyResourcesSelectors } from '@osf/shar MyProjectsTableComponent, IconComponent, TranslatePipe, + ScheduledBannerComponent, LoadingSpinnerComponent, ], templateUrl: './dashboard.component.html', 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 6e2b8f855..7c435dd4d 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 6314795a1..6dfa5b561 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 @@ -24,6 +24,7 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { CustomPaginatorComponent, LoadingSpinnerComponent, + ScheduledBannerComponent, SearchInputComponent, SubHeaderComponent, } from '@osf/shared/components'; @@ -42,6 +43,7 @@ import { FetchInstitutions, InstitutionsSelectors } from '@osf/shared/stores'; 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 800b3548f..378a6080d 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()) { + + } @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 new file mode 100644 index 000000000..996842773 --- /dev/null +++ b/src/app/shared/components/scheduled-banner/scheduled-banner.component.ts @@ -0,0 +1,33 @@ +import { select, Store } from '@ngxs/store'; + +import { ChangeDetectionStrategy, Component, computed, 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', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ScheduledBannerComponent implements OnInit { + private readonly store = inject(Store); + currentBanner = select(BannersSelector.getCurrentBanner); + isMobile = toSignal(inject(IS_XSMALL)); + + ngOnInit() { + this.store.dispatch(new FetchCurrentScheduledBanner()); + } + + shouldShowBanner = computed(() => { + 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/mappers/banner.mapper.ts b/src/app/shared/mappers/banner.mapper.ts new file mode 100644 index 000000000..8526fa40f --- /dev/null +++ b/src/app/shared/mappers/banner.mapper.ts @@ -0,0 +1,20 @@ +import { BannerJsonApi } from '../models/banner.json-api.model'; +import { BannerModel } from '../models/banner.model'; + +export class BannerMapper { + static fromResponse(response: BannerJsonApi): BannerModel { + return { + 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/banner.json-api.model.ts b/src/app/shared/models/banner.json-api.model.ts new file mode 100644 index 000000000..707c896e3 --- /dev/null +++ b/src/app/shared/models/banner.json-api.model.ts @@ -0,0 +1,18 @@ +export interface BannerJsonApi { + 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/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 new file mode 100644 index 000000000..b8eccd401 --- /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 { BannerJsonApi } from '../models/banner.json-api.model'; +import { BannerModel } from '../models/banner.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 the current banner + * + * @returns Observable emitting a Banner object. + * + */ + 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 new file mode 100644 index 000000000..34ffb22f2 --- /dev/null +++ b/src/app/shared/stores/banners/banners.actions.ts @@ -0,0 +1,3 @@ +export class FetchCurrentScheduledBanner { + 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 new file mode 100644 index 000000000..630578cf2 --- /dev/null +++ b/src/app/shared/stores/banners/banners.model.ts @@ -0,0 +1,14 @@ +import { BannerModel } from '@osf/shared/models/banner.model'; +import { AsyncStateModel } from '@shared/models/store'; + +export interface BannersStateModel { + currentBanner: AsyncStateModel; +} + +export const BANNERS_DEFAULTS: BannersStateModel = { + currentBanner: { + data: null, + isLoading: false, + error: null, + }, +}; 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..5a7192821 --- /dev/null +++ b/src/app/shared/stores/banners/banners.selectors.ts @@ -0,0 +1,16 @@ +import { Selector } from '@ngxs/store'; + +import { BannersStateModel } from './banners.model'; +import { BannersState } from './banners.state'; + +export class BannersSelector { + @Selector([BannersState]) + static getCurrentBanner(state: BannersStateModel) { + 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 new file mode 100644 index 000000000..dd153d32f --- /dev/null +++ b/src/app/shared/stores/banners/banners.state.ts @@ -0,0 +1,43 @@ +import { Action, State, StateContext } from '@ngxs/store'; + +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'; +import { BANNERS_DEFAULTS, BannersStateModel } from './banners.model'; + +@State({ + name: 'banners', + defaults: BANNERS_DEFAULTS, +}) +@Injectable() +export class BannersState { + bannersService = inject(BannersService); + + @Action(FetchCurrentScheduledBanner) + fetchCurrentScheduledBanner(ctx: StateContext) { + const state = ctx.getState(); + ctx.patchState({ + currentBanner: { + ...state.currentBanner, + isLoading: true, + }, + }); + return this.bannersService.fetchCurrentBanner().pipe( + tap((newValue) => { + ctx.patchState({ + currentBanner: { + data: newValue, + isLoading: false, + error: null, + }, + }); + catchError((error) => handleSectionError(ctx, 'currentBanner', error)); + }) + ); + } +} 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/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';