Skip to content
Merged
2 changes: 2 additions & 0 deletions src/app/core/constants/ngxs-states.constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -28,4 +29,5 @@ export const STATES = [
MetadataState,
CurrentResourceState,
GlobalSearchState,
BannersState,
];
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
[buttonLabel]="'home.loggedIn.dashboard.createProject' | translate"
(buttonClick)="createProject()"
/>
<osf-scheduled-banner />
<div>
<div class="quick-search-container py-4 px-3 md:px-4">
<p class="text-center mb-4 xl:mb-6">
Expand Down
2 changes: 2 additions & 0 deletions src/app/features/home/pages/dashboard/dashboard.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
IconComponent,
LoadingSpinnerComponent,
MyProjectsTableComponent,
ScheduledBannerComponent,
SubHeaderComponent,
} from '@osf/shared/components';
import { DEFAULT_TABLE_PARAMS } from '@osf/shared/constants';
Expand All @@ -37,6 +38,7 @@ import { ClearMyResources, GetMyProjects, MyResourcesSelectors } from '@osf/shar
MyProjectsTableComponent,
IconComponent,
TranslatePipe,
ScheduledBannerComponent,
LoadingSpinnerComponent,
],
templateUrl: './dashboard.component.html',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[title]="'institutions.title' | translate"
[icon]="'custom-icon-institutions-dark'"
/>

<osf-scheduled-banner />
<div class="flex-column flex flex-1 w-full bg-white p-5">
<osf-search-input [control]="searchControl" [placeholder]="'institutions.searchInstitutions' | translate" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import {
CustomPaginatorComponent,
LoadingSpinnerComponent,
ScheduledBannerComponent,
SearchInputComponent,
SubHeaderComponent,
} from '@osf/shared/components';
Expand All @@ -42,6 +43,7 @@ import { FetchInstitutions, InstitutionsSelectors } from '@osf/shared/stores';
CustomPaginatorComponent,
LoadingSpinnerComponent,
RouterLink,
ScheduledBannerComponent,
],
templateUrl: './institutions-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
[buttonLabel]="'registries.addRegistration' | translate"
(buttonClick)="goToCreateRegistration()"
/>

<osf-scheduled-banner />
<osf-search-input
class="w-full py-4 px-4"
[control]="searchControl"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
SearchInputComponent,
SubHeaderComponent,
} from '@osf/shared/components';
import { ScheduledBannerComponent } from '@osf/shared/components/scheduled-banner/scheduled-banner.component';
import { ResourceType } from '@osf/shared/enums';

import { RegistryServicesComponent } from '../../components';
Expand All @@ -31,6 +32,7 @@ import { environment } from 'src/environments/environment';
ResourceCardComponent,
LoadingSpinnerComponent,
SubHeaderComponent,
ScheduledBannerComponent,
],
templateUrl: './registries-landing.component.html',
styleUrl: './registries-landing.component.scss',
Expand Down
3 changes: 2 additions & 1 deletion src/app/shared/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export { RegistrationCardComponent } from './registration-card/registration-card
export { ResourceCardComponent } from './resource-card/resource-card.component';
export { ResourceMetadataComponent } from './resource-metadata/resource-metadata.component';
export { ReusableFilterComponent } from './reusable-filter/reusable-filter.component';
export { ScheduledBannerComponent } from './scheduled-banner/scheduled-banner.component';
export { SearchHelpTutorialComponent } from './search-help-tutorial/search-help-tutorial.component';
export { SearchInputComponent } from './search-input/search-input.component';
export { SearchResultsContainerComponent } from './search-results-container/search-results-container.component';
Expand All @@ -51,4 +52,4 @@ export { TextInputComponent } from './text-input/text-input.component';
export { ToastComponent } from './toast/toast.component';
export { TruncatedTextComponent } from './truncated-text/truncated-text.component';
export { ViewOnlyLinkMessageComponent } from './view-only-link-message/view-only-link-message.component';
export { ViewOnlyTableComponent } from './view-only-table/view-only-table.component';
export { ViewOnlyTableComponent } from './view-only-table/view-only-table.component';
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@if (this.shouldShowBanner()) {
<div [style.background-color]="this.currentBanner()?.color" class="scheduled-banner-container">
<a [href]="this.currentBanner()?.link" target="_blank" rel="noopener noreferrer">
@if (this.isMobile()) {
<img
class="img-responsive"
[src]="this.currentBanner()?.mobilePhoto"
[alt]="this.currentBanner()?.mobileAltText"
/>
} @else {
<img
class="img-responsive"
[src]="this.currentBanner()?.defaultPhoto"
[alt]="this.currentBanner()?.defaultAltText"
/>
}
</a>
</div>
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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',
styleUrl: './scheduled-banner.component.scss',
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;
});
}
20 changes: 20 additions & 0 deletions src/app/shared/mappers/banner.mapper.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
}
18 changes: 18 additions & 0 deletions src/app/shared/models/banner.json-api.model.ts
Original file line number Diff line number Diff line change
@@ -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;
}
13 changes: 13 additions & 0 deletions src/app/shared/models/banner.model.ts
Original file line number Diff line number Diff line change
@@ -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;
}
38 changes: 38 additions & 0 deletions src/app/shared/services/banners.service.ts
Original file line number Diff line number Diff line change
@@ -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<BannerModel> {
return this.jsonApiService
.get<JsonApiResponse<BannerJsonApi, null>>(`${environment.apiDomainUrl}/_/banners/current`)
.pipe(map((response) => BannerMapper.fromResponse(response.data)));
}
}
3 changes: 3 additions & 0 deletions src/app/shared/stores/banners/banners.actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class FetchCurrentScheduledBanner {
static readonly type = '[Banners] Fetch Current Scheduled Banner';
}
14 changes: 14 additions & 0 deletions src/app/shared/stores/banners/banners.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { BannerModel } from '@osf/shared/models/banner.model';
import { AsyncStateModel } from '@shared/models/store';

export interface BannersStateModel {
currentBanner: AsyncStateModel<BannerModel | null>;
}

export const BANNERS_DEFAULTS: BannersStateModel = {
currentBanner: {
data: null,
isLoading: false,
error: null,
},
};
16 changes: 16 additions & 0 deletions src/app/shared/stores/banners/banners.selectors.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
43 changes: 43 additions & 0 deletions src/app/shared/stores/banners/banners.state.ts
Original file line number Diff line number Diff line change
@@ -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<BannersStateModel>({
name: 'banners',
defaults: BANNERS_DEFAULTS,
})
@Injectable()
export class BannersState {
bannersService = inject(BannersService);

@Action(FetchCurrentScheduledBanner)
fetchCurrentScheduledBanner(ctx: StateContext<BannersStateModel>) {
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));
})
);
}
}
4 changes: 4 additions & 0 deletions src/app/shared/stores/banners/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './banners.actions';
export * from './banners.model';
export * from './banners.selectors';
export * from './banners.state';
1 change: 1 addition & 0 deletions src/app/shared/stores/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './addons';
export * from './banners';
export * from './bookmarks';
export * from './citations';
export * from './collections';
Expand Down
Loading