diff --git a/src/app/core/components/breadcrumb/breadcrumb.component.scss b/src/app/core/components/breadcrumb/breadcrumb.component.scss index 82869f9df..5df4a7926 100644 --- a/src/app/core/components/breadcrumb/breadcrumb.component.scss +++ b/src/app/core/components/breadcrumb/breadcrumb.component.scss @@ -1,7 +1,9 @@ @use "assets/styles/variables" as var; .breadcrumbs { - color: var.$dark-blue-1; + color: var(--header-color, var.$dark-blue-1); font-weight: 600; text-transform: capitalize; + background-color: var(--header-background-color); + background-image: var(--header-background-image-url); } diff --git a/src/app/core/components/header/header.component.html b/src/app/core/components/header/header.component.html index 040f23473..8cadfd34c 100644 --- a/src/app/core/components/header/header.component.html +++ b/src/app/core/components/header/header.component.html @@ -1,14 +1,14 @@
-
@if (isPreprintProviderLoading()) {
- - + +
} @else { -
+
} {{ 'preprints.poweredBy' | translate }}
@@ -32,10 +36,19 @@

{{ 'preprints.title' | translate }}

/> - {{ 'preprints.showExample' | translate }} + @if (isPreprintProviderLoading()) { + + } @else { + {{ 'preprints.showExample' | translate }} + } - + diff --git a/src/app/features/preprints/pages/landing/preprints-landing.component.ts b/src/app/features/preprints/pages/landing/preprints-landing.component.ts index 75eaa3e12..3175b3088 100644 --- a/src/app/features/preprints/pages/landing/preprints-landing.component.ts +++ b/src/app/features/preprints/pages/landing/preprints-landing.component.ts @@ -5,7 +5,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { Skeleton } from 'primeng/skeleton'; -import { ChangeDetectionStrategy, Component, effect, HostBinding, inject, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, effect, HostBinding, inject, OnDestroy, OnInit } from '@angular/core'; import { FormControl } from '@angular/forms'; import { Router, RouterLink } from '@angular/router'; @@ -40,7 +40,7 @@ import { ResourceTab } from '@shared/enums'; styleUrl: './preprints-landing.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class PreprintsLandingComponent implements OnInit { +export class PreprintsLandingComponent implements OnInit, OnDestroy { @HostBinding('class') classes = 'flex-1 flex flex-column w-full h-full'; protected searchControl = new FormControl(''); @@ -53,7 +53,7 @@ export class PreprintsLandingComponent implements OnInit { getHighlightedSubjectsByProviderId: GetHighlightedSubjectsByProviderId, }); - osfPreprintProvider = select(PreprintsSelectors.getPreprintProviderDetails); + osfPreprintProvider = select(PreprintsSelectors.getPreprintProviderDetails(this.OSF_PROVIDER_ID)); isPreprintProviderLoading = select(PreprintsSelectors.isPreprintProviderDetailsLoading); preprintProvidersToAdvertise = select(PreprintsSelectors.getPreprintProvidersToAdvertise); highlightedSubjectsByProviderId = select(PreprintsSelectors.getHighlightedSubjectsForProvider); @@ -79,6 +79,10 @@ export class PreprintsLandingComponent implements OnInit { this.actions.getHighlightedSubjectsByProviderId(this.OSF_PROVIDER_ID); } + ngOnDestroy() { + BrandService.resetBranding(); + } + redirectToSearchPageWithValue() { const searchValue = this.searchControl.value; diff --git a/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.html b/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.html new file mode 100644 index 000000000..c0f7884d4 --- /dev/null +++ b/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.html @@ -0,0 +1,14 @@ + + + + + + + diff --git a/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.scss b/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.spec.ts b/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.spec.ts new file mode 100644 index 000000000..70d112413 --- /dev/null +++ b/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PreprintProviderOverviewComponent } from './preprint-provider-overview.component'; + +describe('ProviderOverviewComponent', () => { + let component: PreprintProviderOverviewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PreprintProviderOverviewComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(PreprintProviderOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.ts b/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.ts new file mode 100644 index 000000000..44c5b0fdd --- /dev/null +++ b/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.ts @@ -0,0 +1,70 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { map, of } from 'rxjs'; + +import { ChangeDetectionStrategy, Component, effect, inject, OnDestroy, OnInit } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { ActivatedRoute } from '@angular/router'; + +import { AdvisoryBoardComponent, BrowseBySubjectsComponent } from '@osf/features/preprints/components'; +import { PreprintProviderFooterComponent } from '@osf/features/preprints/components/preprint-provider-footer/preprint-provider-footer.component'; +import { PreprintProviderHeroComponent } from '@osf/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component'; +import { BrandService } from '@osf/features/preprints/services'; +import { + GetHighlightedSubjectsByProviderId, + GetPreprintProviderById, + PreprintsSelectors, +} from '@osf/features/preprints/store'; +import { HeaderStyleHelper } from '@shared/services'; + +@Component({ + selector: 'osf-provider-overview', + imports: [ + AdvisoryBoardComponent, + BrowseBySubjectsComponent, + PreprintProviderHeroComponent, + PreprintProviderFooterComponent, + ], + templateUrl: './preprint-provider-overview.component.html', + styleUrl: './preprint-provider-overview.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PreprintProviderOverviewComponent implements OnInit, OnDestroy { + private readonly route = inject(ActivatedRoute); + private providerId = toSignal(this.route.params.pipe(map((params) => params['providerId'])) ?? of(undefined)); + + private actions = createDispatchMap({ + getPreprintProviderById: GetPreprintProviderById, + getHighlightedSubjectsByProviderId: GetHighlightedSubjectsByProviderId, + }); + preprintProvider = select(PreprintsSelectors.getPreprintProviderDetails(this.providerId())); + isPreprintProviderLoading = select(PreprintsSelectors.isPreprintProviderDetailsLoading); + + highlightedSubjectsByProviderId = select(PreprintsSelectors.getHighlightedSubjectsForProvider); + areSubjectsLoading = select(PreprintsSelectors.areSubjectsLoading); + + constructor() { + effect(() => { + const provider = this.preprintProvider(); + + if (provider) { + BrandService.applyBranding(provider.brand); + HeaderStyleHelper.applyHeaderStyles( + provider.brand.primaryColor, + provider.brand.secondaryColor, + provider.brand.heroBackgroundImageUrl + ); + } + }); + } + + ngOnInit() { + this.actions.getPreprintProviderById(this.providerId()); + this.actions.getHighlightedSubjectsByProviderId(this.providerId()); + } + + ngOnDestroy() { + HeaderStyleHelper.resetToDefaults(); + BrandService.resetBranding(); + } +} diff --git a/src/app/features/preprints/preprints.component.scss b/src/app/features/preprints/preprints.component.scss index f28af4ffb..bf9f05029 100644 --- a/src/app/features/preprints/preprints.component.scss +++ b/src/app/features/preprints/preprints.component.scss @@ -3,5 +3,5 @@ .desktop { @include mix.flex-column; flex: 1; - margin-top: mix.rem(70px); + margin-top: mix.rem(48px); } diff --git a/src/app/features/preprints/services/brand.service.ts b/src/app/features/preprints/services/brand.service.ts index ece0b5c72..e4f885566 100644 --- a/src/app/features/preprints/services/brand.service.ts +++ b/src/app/features/preprints/services/brand.service.ts @@ -8,6 +8,14 @@ export class BrandService { root.style.setProperty('--preprints-branding-secondary-color', brand.secondaryColor); root.style.setProperty('--preprints-branding-hero-logo-image-url', `url(${brand.topNavLogoImageUrl})`); root.style.setProperty('--preprints-branding-hero-background-image-url', `url(${brand.heroBackgroundImageUrl})`); - root.style.setProperty('--preprints-branding-top-nav-image-url', `url(${brand.heroBackgroundImageUrl})`); + } + + static resetBranding(): void { + const root = document.documentElement; + + root.style.setProperty('--preprints-branding-primary-color', ''); + root.style.setProperty('--preprints-branding-secondary-color', ''); + root.style.setProperty('--preprints-branding-hero-logo-image-url', ''); + root.style.setProperty('--preprints-branding-hero-background-image-url', ''); } } diff --git a/src/app/features/preprints/services/preprints.service.ts b/src/app/features/preprints/services/preprints.service.ts index 6dd185f05..d83cce72b 100644 --- a/src/app/features/preprints/services/preprints.service.ts +++ b/src/app/features/preprints/services/preprints.service.ts @@ -51,7 +51,7 @@ export class PreprintsService { >(`${this.baseUrl}${providerId}/subjects/highlighted/?page[size]=20`) .pipe( map((response) => { - return PreprintsMapper.fromSubjectsGetResponse(response.data); + return PreprintsMapper.fromSubjectsGetResponse(providerId, response.data); }) ); } diff --git a/src/app/features/preprints/store/preprints.model.ts b/src/app/features/preprints/store/preprints.model.ts index 45a150f0d..4a39aca96 100644 --- a/src/app/features/preprints/store/preprints.model.ts +++ b/src/app/features/preprints/store/preprints.model.ts @@ -2,7 +2,7 @@ import { PreprintProviderDetails, PreprintProviderToAdvertise, Subject } from '@ import { AsyncStateModel } from '@shared/models'; export interface PreprintsStateModel { - preprintProviderDetails: AsyncStateModel; + preprintProvidersDetails: AsyncStateModel; preprintProvidersToAdvertise: AsyncStateModel; highlightedSubjectsForProvider: AsyncStateModel; } diff --git a/src/app/features/preprints/store/preprints.selectors.ts b/src/app/features/preprints/store/preprints.selectors.ts index b2e0bcee9..4e3732f72 100644 --- a/src/app/features/preprints/store/preprints.selectors.ts +++ b/src/app/features/preprints/store/preprints.selectors.ts @@ -4,13 +4,14 @@ import { PreprintsState, PreprintsStateModel } from '@osf/features/preprints/sto export class PreprintsSelectors { @Selector([PreprintsState]) - static getPreprintProviderDetails(state: PreprintsStateModel) { - return state.preprintProviderDetails.data; + static getPreprintProviderDetails(providerId: string) { + return (state: { preprints: PreprintsStateModel }) => + state.preprints.preprintProvidersDetails.data.find((provider) => provider.id.includes(providerId)); } @Selector([PreprintsState]) static isPreprintProviderDetailsLoading(state: PreprintsStateModel) { - return state.preprintProviderDetails.isLoading; + return state.preprintProvidersDetails.isLoading; } @Selector([PreprintsState]) diff --git a/src/app/features/preprints/store/preprints.state.ts b/src/app/features/preprints/store/preprints.state.ts index deaef2f5c..261f65a61 100644 --- a/src/app/features/preprints/store/preprints.state.ts +++ b/src/app/features/preprints/store/preprints.state.ts @@ -1,7 +1,7 @@ import { Action, State, StateContext } from '@ngxs/store'; -import { patch } from '@ngxs/store/operators'; +import { insertItem, patch, updateItem } from '@ngxs/store/operators'; -import { tap, throwError } from 'rxjs'; +import { of, tap, throwError } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { inject, Injectable } from '@angular/core'; @@ -17,8 +17,8 @@ import { PreprintsStateModel } from '@osf/features/preprints/store/preprints.mod @State({ name: 'preprints', defaults: { - preprintProviderDetails: { - data: null, + preprintProvidersDetails: { + data: [], isLoading: false, error: null, }, @@ -37,23 +37,37 @@ import { PreprintsStateModel } from '@osf/features/preprints/store/preprints.mod @Injectable() export class PreprintsState { #preprintsService = inject(PreprintsService); + private readonly REFRESH_INTERVAL = 5 * 60 * 1000; @Action(GetPreprintProviderById) getPreprintProviderById(ctx: StateContext, action: GetPreprintProviderById) { - ctx.setState(patch({ preprintProviderDetails: patch({ isLoading: true }) })); + const state = ctx.getState(); + const cachedData = state.preprintProvidersDetails.data.find((p) => p.id === action.id); + const shouldRefresh = this.shouldRefresh(cachedData?.lastFetched); + + if (cachedData && !shouldRefresh) { + return of(cachedData); + } + + ctx.setState(patch({ preprintProvidersDetails: patch({ isLoading: true }) })); return this.#preprintsService.getPreprintProviderById(action.id).pipe( - tap((data) => { + tap((preprintProvider) => { + const exists = state.preprintProvidersDetails.data.some((p) => p.id === preprintProvider.id); + preprintProvider.lastFetched = Date.now(); + ctx.setState( patch({ - preprintProviderDetails: patch({ - data: data, + preprintProvidersDetails: patch({ + data: exists + ? updateItem((p) => p.id === preprintProvider.id, preprintProvider) + : insertItem(preprintProvider), isLoading: false, }), }) ); }), - catchError((error) => this.handleError(ctx, 'preprintProviderDetails', error)) + catchError((error) => this.handleError(ctx, 'preprintProvidersDetails', error)) ); } @@ -84,11 +98,11 @@ export class PreprintsState { ctx.setState(patch({ highlightedSubjectsForProvider: patch({ isLoading: true }) })); return this.#preprintsService.getHighlightedSubjectsByProviderId(action.providerId).pipe( - tap((data) => { + tap((subjects) => { ctx.setState( patch({ highlightedSubjectsForProvider: patch({ - data: data, + data: subjects, isLoading: false, }), }) @@ -98,6 +112,14 @@ export class PreprintsState { ); } + private shouldRefresh(lastFetched: number | undefined): boolean { + if (!lastFetched) { + return true; + } + + return Date.now() - lastFetched > this.REFRESH_INTERVAL; + } + private handleError(ctx: StateContext, section: keyof PreprintsStateModel, error: Error) { ctx.patchState({ [section]: { diff --git a/src/app/shared/pipes/decode-html.pipe.ts b/src/app/shared/pipes/decode-html.pipe.ts new file mode 100644 index 000000000..abcfdd5dc --- /dev/null +++ b/src/app/shared/pipes/decode-html.pipe.ts @@ -0,0 +1,13 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'decodeHtml', + standalone: true, +}) +export class DecodeHtmlPipe implements PipeTransform { + transform(value: string): string { + if (!value) return ''; + + return value.replace(/</g, '<').replace(/>/g, '>'); + } +} diff --git a/src/app/shared/pipes/index.ts b/src/app/shared/pipes/index.ts index 83261cfc4..05dc24e77 100644 --- a/src/app/shared/pipes/index.ts +++ b/src/app/shared/pipes/index.ts @@ -1,3 +1,4 @@ +export { DecodeHtmlPipe } from './decode-html.pipe'; export { FileSizePipe } from './file-size.pipe'; export { MonthYearPipe } from './month-year.pipe'; export { WrapFnPipe } from './wrap-fn.pipe'; diff --git a/src/app/shared/utils/header-style.helper.ts b/src/app/shared/utils/header-style.helper.ts new file mode 100644 index 000000000..e2d93d0b9 --- /dev/null +++ b/src/app/shared/utils/header-style.helper.ts @@ -0,0 +1,17 @@ +export class HeaderStyleHelper { + static applyHeaderStyles(textColor: string, backgroundColor?: string, backgroundImageUrl?: string) { + const root = document.documentElement; + + root.style.setProperty('--header-color', textColor); + root.style.setProperty('--header-background-color', backgroundColor || ''); + root.style.setProperty('--header-background-image-url', backgroundImageUrl || ''); + } + + static resetToDefaults() { + const root = document.documentElement; + + root.style.setProperty('--header-color', ''); + root.style.setProperty('--header-background-color', ''); + root.style.setProperty('--header-background-image-url', ''); + } +} diff --git a/src/app/shared/utils/index.ts b/src/app/shared/utils/index.ts index 102d6a294..ec1ea68b3 100644 --- a/src/app/shared/utils/index.ts +++ b/src/app/shared/utils/index.ts @@ -1,3 +1,4 @@ +export { HeaderStyleHelper } from '../utils/header-style.helper'; export * from './add-filters-params.helper'; export * from './breakpoints.tokens'; export * from './custom-form-validators.helper'; diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index e54bb218b..b3b25e509 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1230,7 +1230,7 @@ }, "preprints": { "title": "Preprints", - "addPreprint": "Add a Preprint", + "addPreprint": "Add a {{preprintWord}}", "poweredBy": "Powered by OSF Preprints", "searchPlaceholder": "Search Preprints", "showExample": "Show an example", @@ -1249,6 +1249,11 @@ "publicRoadmap": "public roadmap", "inputWelcome": "Input welcome!", "contactUs": "Contact us" + }, + "helpDialog": { + "header": "Search help", + "message": "OSF Search provides a powerful discovery tool to help you find data, papers, analysis plans, and more content across the research lifecycle. OSF Preprints has some specialized filters, which you can learn more about on our", + "linkText": "help guides" } }, "truncatedText": { diff --git a/src/assets/styles/_variables.scss b/src/assets/styles/_variables.scss index 0e82b5fa6..2e2f0f38d 100644 --- a/src/assets/styles/_variables.scss +++ b/src/assets/styles/_variables.scss @@ -137,9 +137,12 @@ $white-60: var(--white-60); #f3f8fd; --gradient-3: linear-gradient(90.12deg, #dbedfb 0.03%, #eff4fc 54.38%, #dbedfb 98.07%); + --header-color: none; + --header-background-color: none; + --header-background-image-url: none; + --preprints-branding-primary-color: var(--pr-blue-1); - --preprints-branding-secondary-color: var(--pr-blue-1); + --preprints-branding-secondary-color: var(--white); --preprints-branding-hero-logo-image-url: none; --preprints-branding-hero-background-image-url: none; - --preprints-branding-top-nav-image-url: none; } diff --git a/src/assets/styles/components/advisory-board.scss b/src/assets/styles/components/advisory-board.scss deleted file mode 100644 index 85fae76d4..000000000 --- a/src/assets/styles/components/advisory-board.scss +++ /dev/null @@ -1,17 +0,0 @@ -@use "assets/styles/mixins" as mix; - -.advisory-board-section { - h2 { - font-size: mix.rem(24px); - margin-bottom: mix.rem(10px); - } - - ul { - margin-left: mix.rem(24px); - line-height: mix.rem(24px); - - li { - list-style: disc; - } - } -} diff --git a/src/assets/styles/components/preprints.scss b/src/assets/styles/components/preprints.scss new file mode 100644 index 000000000..db2b59aca --- /dev/null +++ b/src/assets/styles/components/preprints.scss @@ -0,0 +1,108 @@ +@use "assets/styles/mixins" as mix; +@use "assets/styles/variables" as var; + +.preprints-hero-container { + background-color: var(--preprints-branding-secondary-color); + background-image: var(--preprints-branding-hero-background-image-url); + + color: var(--preprints-branding-primary-color); + + .preprint-provider-name { + color: var(--preprints-branding-primary-color); + } + + .provider-description { + line-height: mix.rem(24px); + } + + a { + color: var(--preprints-branding-primary-color); + } + + .preprints-branding-button { + .p-button { + color: var(--preprints-branding-secondary-color); + background-color: var(--preprints-branding-primary-color); + } + } +} + +.preprints-advisory-board-section { + background: var(--preprints-branding-hero-background-image-url); + + h2 { + font-size: mix.rem(24px); + margin-top: mix.rem(20px); + margin-bottom: mix.rem(10px); + } + + ul { + margin-left: mix.rem(24px); + margin-bottom: mix.rem(10px); + line-height: mix.rem(24px); + + li { + list-style: disc; + } + } + + .preprint-advisory-list { + flex-direction: row; + display: flex; + justify-content: flex-start; + align-items: flex-start; + margin-top: mix.rem(24px); + + .preprint-advisory-list-column { + width: 50%; + + li { + list-style: none; + font-size: 16px; + } + } + } + + @media (max-width: var.$breakpoint-md) { + .preprint-advisory-list { + flex-direction: column; + + .preprint-advisory-list-column { + width: 100%; + } + } + } + + &.osf-preprint-service { + background-image: none; + background-color: var(--preprints-branding-primary-color); + } +} + +.preprints-browse-by-subjects-section { + .provider-subject { + .p-button.p-button-secondary { + color: var(--preprints-branding-secondary-color); + background-color: var(--preprints-branding-primary-color); + border: 1px solid var(--preprints-branding-secondary-color); + + &:hover { + color: var(--preprints-branding-primary-color); + border: 1px solid var(--preprints-branding-primary-color); + background-color: var(--preprints-branding-secondary-color); + } + } + } +} + +.preprints-footer-section { + padding-top: mix.rem(10px); + + a { + font-weight: bold; + } + + p { + margin-bottom: mix.rem(10px); + } +} diff --git a/src/assets/styles/overrides/button.scss b/src/assets/styles/overrides/button.scss index 9b4ab9313..17e25996e 100644 --- a/src/assets/styles/overrides/button.scss +++ b/src/assets/styles/overrides/button.scss @@ -245,7 +245,7 @@ } } -.dropdown-button { +.header-dropdown-button { position: relative; display: flex; flex-direction: column; @@ -253,7 +253,7 @@ .p-button { background: transparent; - color: var.$dark-blue-2; + color: var(--header-color, var.$dark-blue-1); padding: 0; } diff --git a/src/assets/styles/styles.scss b/src/assets/styles/styles.scss index 395c5336f..3b09e0b52 100644 --- a/src/assets/styles/styles.scss +++ b/src/assets/styles/styles.scss @@ -38,4 +38,4 @@ @use "./overrides/toast"; @use "./overrides/primeflex-override"; @use "./overrides/multiselect"; -@use "./components/advisory-board"; +@use "components/preprints";