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';