diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index adbad36dd..71073dad3 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -159,6 +159,11 @@ export const routes: Routes = [ path: 'settings', loadChildren: () => import('./features/settings/settings.routes').then((mod) => mod.settingsRoutes), }, + { + path: 'preprints', + loadChildren: () => + import('./features/preprints/constants/preprints.routes').then((mod) => mod.preprintsRoutes), + }, { path: 'search', loadComponent: () => import('./features/search/search.component').then((mod) => mod.SearchComponent), diff --git a/src/app/core/constants/nav-items.constant.ts b/src/app/core/constants/nav-items.constant.ts index c3c68d64f..9fabf8d12 100644 --- a/src/app/core/constants/nav-items.constant.ts +++ b/src/app/core/constants/nav-items.constant.ts @@ -28,6 +28,12 @@ export const NAV_ITEMS: NavItem[] = [ icon: 'my-projects', useExactMatch: true, }, + { + path: '/preprints', + label: 'navigation.preprints', + icon: 'preprints', + useExactMatch: true, + }, { path: '/my-profile', label: 'navigation.profile', diff --git a/src/app/core/constants/ngxs-states.constant.ts b/src/app/core/constants/ngxs-states.constant.ts index 59d7fb9b9..c0b578a4f 100644 --- a/src/app/core/constants/ngxs-states.constant.ts +++ b/src/app/core/constants/ngxs-states.constant.ts @@ -4,6 +4,7 @@ import { CollectionsState } from '@osf/features/collections/store'; import { InstitutionsState } from '@osf/features/institutions/store'; import { MeetingsState } from '@osf/features/meetings/store'; import { MyProjectsState } from '@osf/features/my-projects/store'; +import { PreprintsState } from '@osf/features/preprints/store'; import { AnalyticsState } from '@osf/features/project/analytics/store'; import { ProjectOverviewState } from '@osf/features/project/overview/store'; import { SettingsState } from '@osf/features/project/settings/store'; @@ -32,4 +33,5 @@ export const STATES = [ CollectionsState, WikiState, MeetingsState, + PreprintsState, ]; diff --git a/src/app/features/auth/pages/forgot-password/forgot-password.component.scss b/src/app/features/auth/pages/forgot-password/forgot-password.component.scss index 3481b9ff6..6926c3fb8 100644 --- a/src/app/features/auth/pages/forgot-password/forgot-password.component.scss +++ b/src/app/features/auth/pages/forgot-password/forgot-password.component.scss @@ -4,7 +4,7 @@ :host { @include mix.flex-center; flex: 1; - background: url("/assets/images/auth-background.png") center no-repeat; + background: url("/assets/images/dark-blue-gradient.png") center no-repeat; background-size: cover; .forgot-password-container { diff --git a/src/app/features/auth/pages/reset-password/reset-password.component.scss b/src/app/features/auth/pages/reset-password/reset-password.component.scss index d85b37855..1bc67d621 100644 --- a/src/app/features/auth/pages/reset-password/reset-password.component.scss +++ b/src/app/features/auth/pages/reset-password/reset-password.component.scss @@ -4,7 +4,7 @@ :host { @include mix.flex-center; flex: 1; - background: url("/assets/images/auth-background.png") center no-repeat; + background: url("/assets/images/dark-blue-gradient.png") center no-repeat; background-size: cover; .reset-password-container, diff --git a/src/app/features/auth/pages/sign-up/sign-up.component.scss b/src/app/features/auth/pages/sign-up/sign-up.component.scss index 8e40212dd..85de9bafc 100644 --- a/src/app/features/auth/pages/sign-up/sign-up.component.scss +++ b/src/app/features/auth/pages/sign-up/sign-up.component.scss @@ -4,7 +4,7 @@ :host { @include mix.flex-column-center; flex: 1; - background: url("/assets/images/auth-background.png") center no-repeat; + background: url("/assets/images/dark-blue-gradient.png") center no-repeat; background-size: cover; .sign-up-container, diff --git a/src/app/features/preprints/components/advisory-board/advisory-board.component.html b/src/app/features/preprints/components/advisory-board/advisory-board.component.html new file mode 100644 index 000000000..21e9aabfd --- /dev/null +++ b/src/app/features/preprints/components/advisory-board/advisory-board.component.html @@ -0,0 +1,7 @@ +@if (htmlContent()) { +
+} diff --git a/src/app/features/preprints/components/advisory-board/advisory-board.component.scss b/src/app/features/preprints/components/advisory-board/advisory-board.component.scss new file mode 100644 index 000000000..4526d8747 --- /dev/null +++ b/src/app/features/preprints/components/advisory-board/advisory-board.component.scss @@ -0,0 +1,15 @@ +@use "assets/styles/variables" as var; +@use "assets/styles/mixins" as mix; + +.advisory-board-section { + background: var(--preprints-branding-hero-background-image-url); + + &.osf-preprint-service { + background-image: none; + background-color: var(--preprints-branding-primary-color); + } +} + +.m-b-lg { + margin-bottom: mix.rem(24px); +} diff --git a/src/app/features/preprints/components/advisory-board/advisory-board.component.spec.ts b/src/app/features/preprints/components/advisory-board/advisory-board.component.spec.ts new file mode 100644 index 000000000..4b3b51231 --- /dev/null +++ b/src/app/features/preprints/components/advisory-board/advisory-board.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AdvisoryBoardComponent } from './advisory-board.component'; + +describe('AdvisoryGroupComponent', () => { + let component: AdvisoryBoardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AdvisoryBoardComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(AdvisoryBoardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/preprints/components/advisory-board/advisory-board.component.ts b/src/app/features/preprints/components/advisory-board/advisory-board.component.ts new file mode 100644 index 000000000..8471e964d --- /dev/null +++ b/src/app/features/preprints/components/advisory-board/advisory-board.component.ts @@ -0,0 +1,18 @@ +import { NgClass } from '@angular/common'; +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; + +import { StringOrNullOrUndefined } from '@core/helpers'; +import { Brand } from '@osf/features/preprints/models'; + +@Component({ + selector: 'osf-advisory-board', + imports: [NgClass], + templateUrl: './advisory-board.component.html', + styleUrl: './advisory-board.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AdvisoryBoardComponent { + htmlContent = input(null); + brand = input(); + isLandingPage = input(false); +} diff --git a/src/app/features/preprints/components/browse-by-subjects/browse-by-subjects.component.html b/src/app/features/preprints/components/browse-by-subjects/browse-by-subjects.component.html new file mode 100644 index 000000000..c7065f18a --- /dev/null +++ b/src/app/features/preprints/components/browse-by-subjects/browse-by-subjects.component.html @@ -0,0 +1,26 @@ +
+

{{ 'preprints.browseBySubjects.title' | translate }}

+
+ @if (isLoading()) { + @for (_ of skeletonArray; track $index) { + +
+ +
+
+ } + } @else { + @for (subject of subjects(); track subject) { + + + + } + } +
+
diff --git a/src/app/features/preprints/components/browse-by-subjects/browse-by-subjects.component.scss b/src/app/features/preprints/components/browse-by-subjects/browse-by-subjects.component.scss new file mode 100644 index 000000000..e52c50881 --- /dev/null +++ b/src/app/features/preprints/components/browse-by-subjects/browse-by-subjects.component.scss @@ -0,0 +1,7 @@ +@use "assets/styles/variables" as var; + +.provider-subject { + .subject-link { + color: var.$dark-blue-1; + } +} diff --git a/src/app/features/preprints/components/browse-by-subjects/browse-by-subjects.component.spec.ts b/src/app/features/preprints/components/browse-by-subjects/browse-by-subjects.component.spec.ts new file mode 100644 index 000000000..3f73b1161 --- /dev/null +++ b/src/app/features/preprints/components/browse-by-subjects/browse-by-subjects.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BrowseBySubjectsComponent } from './browse-by-subjects.component'; + +describe('BrowseBySubjectsComponent', () => { + let component: BrowseBySubjectsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [BrowseBySubjectsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(BrowseBySubjectsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/preprints/components/browse-by-subjects/browse-by-subjects.component.ts b/src/app/features/preprints/components/browse-by-subjects/browse-by-subjects.component.ts new file mode 100644 index 000000000..59681020b --- /dev/null +++ b/src/app/features/preprints/components/browse-by-subjects/browse-by-subjects.component.ts @@ -0,0 +1,37 @@ +import { TranslateModule } from '@ngx-translate/core'; + +import { Card } from 'primeng/card'; +import { Skeleton } from 'primeng/skeleton'; + +import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; +import { RouterLink } from '@angular/router'; + +import { Subject } from '@osf/features/preprints/models'; +import { ResourceTab } from '@shared/enums'; + +import { environment } from 'src/environments/environment'; + +@Component({ + selector: 'osf-browse-by-subjects', + imports: [Card, RouterLink, Skeleton, TranslateModule], + templateUrl: './browse-by-subjects.component.html', + styleUrl: './browse-by-subjects.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class BrowseBySubjectsComponent { + subjects = input.required(); + linksToSearchPageForSubject = computed(() => { + return this.subjects().map((subject) => ({ + resourceTab: ResourceTab.Preprints, + activeFilters: JSON.stringify([ + { + filterName: 'Subject', + label: subject.text, + value: `${environment.apiUrl}/subjects/` + subject.id, + }, + ]), + })); + }); + isLoading = input.required(); + skeletonArray = Array.from({ length: 10 }, (_, i) => i + 1); +} diff --git a/src/app/features/preprints/components/index.ts b/src/app/features/preprints/components/index.ts new file mode 100644 index 000000000..6eeb82459 --- /dev/null +++ b/src/app/features/preprints/components/index.ts @@ -0,0 +1,3 @@ +export { BrowseBySubjectsComponent } from './browse-by-subjects/browse-by-subjects.component'; +export { PreprintServicesComponent } from './preprint-services/preprint-services.component'; +export { AdvisoryBoardComponent } from '@osf/features/preprints/components/advisory-board/advisory-board.component'; diff --git a/src/app/features/preprints/components/preprint-services/preprint-services.component.html b/src/app/features/preprints/components/preprint-services/preprint-services.component.html new file mode 100644 index 000000000..4c43ca044 --- /dev/null +++ b/src/app/features/preprints/components/preprint-services/preprint-services.component.html @@ -0,0 +1,17 @@ +
+

{{ 'preprints.services.title' | translate }}

+

+ {{ 'preprints.services.description' | translate }} +

+ +
+ @for (preprintProvider of preprintProvidersToAdvertise(); track preprintProvider.id) { + + + } +
+
diff --git a/src/app/features/preprints/components/preprint-services/preprint-services.component.scss b/src/app/features/preprints/components/preprint-services/preprint-services.component.scss new file mode 100644 index 000000000..c13d7bb7f --- /dev/null +++ b/src/app/features/preprints/components/preprint-services/preprint-services.component.scss @@ -0,0 +1,14 @@ +@use "assets/styles/mixins" as mix; + +.services-background-image { + background: url("/assets/images/preprints/preprints-services-background.png") center; +} + +.preprint-provider-grid-item { + display: block; + width: mix.rem(140px); + height: mix.rem(70px); + background-size: contain; + background-repeat: no-repeat; + background-position: center; +} diff --git a/src/app/features/preprints/components/preprint-services/preprint-services.component.spec.ts b/src/app/features/preprints/components/preprint-services/preprint-services.component.spec.ts new file mode 100644 index 000000000..ab3f2d5a9 --- /dev/null +++ b/src/app/features/preprints/components/preprint-services/preprint-services.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PreprintServicesComponent } from './preprint-services.component'; + +describe('PreprintServicesComponent', () => { + let component: PreprintServicesComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PreprintServicesComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(PreprintServicesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/preprints/components/preprint-services/preprint-services.component.ts b/src/app/features/preprints/components/preprint-services/preprint-services.component.ts new file mode 100644 index 000000000..076fcd88f --- /dev/null +++ b/src/app/features/preprints/components/preprint-services/preprint-services.component.ts @@ -0,0 +1,16 @@ +import { TranslateModule } from '@ngx-translate/core'; + +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; + +import { PreprintProviderToAdvertise } from '@osf/features/preprints/models'; + +@Component({ + selector: 'osf-preprint-services', + imports: [TranslateModule], + templateUrl: './preprint-services.component.html', + styleUrl: './preprint-services.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PreprintServicesComponent { + preprintProvidersToAdvertise = input.required(); +} diff --git a/src/app/features/preprints/constants/preprints.routes.ts b/src/app/features/preprints/constants/preprints.routes.ts new file mode 100644 index 000000000..7dc75b0c8 --- /dev/null +++ b/src/app/features/preprints/constants/preprints.routes.ts @@ -0,0 +1,24 @@ +import { Routes } from '@angular/router'; + +import { PreprintsComponent } from '@osf/features/preprints/preprints.component'; + +export const preprintsRoutes: Routes = [ + { + path: '', + component: PreprintsComponent, + children: [ + { + path: '', + pathMatch: 'full', + redirectTo: 'overview', + }, + { + path: 'overview', + loadComponent: () => + import('@osf/features/preprints/pages/landing/preprints-landing.component').then( + (c) => c.PreprintsLandingComponent + ), + }, + ], + }, +]; diff --git a/src/app/features/preprints/mappers/index.ts b/src/app/features/preprints/mappers/index.ts new file mode 100644 index 000000000..7ed240bce --- /dev/null +++ b/src/app/features/preprints/mappers/index.ts @@ -0,0 +1 @@ +export * from './preprints.mapper'; diff --git a/src/app/features/preprints/mappers/preprints.mapper.ts b/src/app/features/preprints/mappers/preprints.mapper.ts new file mode 100644 index 000000000..4b8069729 --- /dev/null +++ b/src/app/features/preprints/mappers/preprints.mapper.ts @@ -0,0 +1,52 @@ +import { + PreprintProviderDetails, + PreprintProviderDetailsGetResponse, + PreprintProviderToAdvertise, + Subject, + SubjectGetResponse, +} from '@osf/features/preprints/models'; + +export class PreprintsMapper { + static fromPreprintProviderDetailsGetResponse(response: PreprintProviderDetailsGetResponse): PreprintProviderDetails { + const brandRaw = response.embeds!.brand.data; + return { + id: response.id, + name: response.attributes.name, + descriptionHtml: response.attributes.description, + advisoryBoardHtml: response.attributes.advisory_board, + examplePreprintId: response.attributes.example, + domain: response.attributes.domain, + footerLinksHtml: response.attributes.footer_links, + preprintWord: response.attributes.preprint_word, + brand: { + id: brandRaw.id, + name: brandRaw.attributes.name, + heroLogoImageUrl: brandRaw.attributes.hero_logo_image, + heroBackgroundImageUrl: brandRaw.attributes.hero_background_image, + topNavLogoImageUrl: brandRaw.attributes.topnav_logo_image, + primaryColor: brandRaw.attributes.primary_color, + secondaryColor: brandRaw.attributes.secondary_color, + }, + }; + } + + static fromPreprintProvidersToAdvertiseGetResponse( + response: PreprintProviderDetailsGetResponse[] + ): PreprintProviderToAdvertise[] { + return response + .filter((item) => !item.id.includes('osf')) + .map((item) => ({ + id: item.id, + name: item.attributes.name, + whiteWideImageUrl: item.attributes.assets.wide_white, + })); + } + + static fromSubjectsGetResponse(response: SubjectGetResponse[]): Subject[] { + return response.map((subject) => ({ + id: subject.id, + text: subject.attributes.text, + taxonomy_name: subject.attributes.taxonomy_name, + })); + } +} diff --git a/src/app/features/preprints/models/index.ts b/src/app/features/preprints/models/index.ts new file mode 100644 index 000000000..946c7656c --- /dev/null +++ b/src/app/features/preprints/models/index.ts @@ -0,0 +1 @@ +export * from './preprints.models'; diff --git a/src/app/features/preprints/models/preprints.models.ts b/src/app/features/preprints/models/preprints.models.ts new file mode 100644 index 000000000..eb9789d31 --- /dev/null +++ b/src/app/features/preprints/models/preprints.models.ts @@ -0,0 +1,81 @@ +import { StringOrNull } from '@core/helpers'; + +// domain models +export interface Brand { + id: string; + name: string; + heroLogoImageUrl: string; + heroBackgroundImageUrl: string; + topNavLogoImageUrl: string; + primaryColor: string; + secondaryColor: string; +} + +export interface PreprintProviderDetails { + id: string; + name: string; + descriptionHtml: string; + advisoryBoardHtml: StringOrNull; + examplePreprintId: string; + domain: string; + footerLinksHtml: string; + preprintWord: string; + brand: Brand; +} + +export interface PreprintProviderToAdvertise { + id: string; + name: string; + whiteWideImageUrl: string; +} + +export interface Subject { + id: string; + text: string; + taxonomy_name: string; +} + +//api models +export interface PreprintProviderDetailsGetResponse { + id: string; + type: 'preprint-providers'; + attributes: { + name: string; + description: string; + advisory_board: StringOrNull; + example: string; + domain: string; + footer_links: string; + preprint_word: string; + assets: { + wide_white: string; + }; + }; + embeds?: { + brand: { + data: BrandGetResponse; + }; + }; +} + +export interface BrandGetResponse { + id: string; + type: 'brands'; + attributes: { + name: string; + hero_logo_image: string; + hero_background_image: string; + topnav_logo_image: string; + primary_color: string; + secondary_color: string; + }; +} + +export interface SubjectGetResponse { + id: string; + type: 'subjects'; + attributes: { + text: string; + taxonomy_name: string; + }; +} diff --git a/src/app/features/preprints/pages/index.ts b/src/app/features/preprints/pages/index.ts new file mode 100644 index 000000000..ae9e70859 --- /dev/null +++ b/src/app/features/preprints/pages/index.ts @@ -0,0 +1 @@ +export { PreprintsLandingComponent } from '@osf/features/preprints/pages/landing/preprints-landing.component'; diff --git a/src/app/features/preprints/pages/landing/preprints-landing.component.html b/src/app/features/preprints/pages/landing/preprints-landing.component.html new file mode 100644 index 000000000..ddec14bae --- /dev/null +++ b/src/app/features/preprints/pages/landing/preprints-landing.component.html @@ -0,0 +1,69 @@ +
+
+
+ +

{{ 'preprints.title' | translate }}

+
+ +
+ +
+ @if (isPreprintProviderLoading()) { +
+ + +
+ } @else { +
+ } + {{ 'preprints.poweredBy' | translate }} +
+ + + + + {{ 'preprints.showExample' | translate }} +
+ + + + + + + +
+
+

{{ 'preprints.createServer.title' | translate }}

+

+ {{ 'preprints.createServer.description' | translate }} + {{ + 'preprints.createServer.openSourceCode' | translate + }} + {{ 'preprints.createServer.and' | translate }} + {{ + 'preprints.createServer.publicRoadmap' | translate + }}. {{ 'preprints.createServer.inputWelcome' | translate }} +

+
+ + + {{ 'preprints.createServer.contactUs' | translate }} + +
diff --git a/src/app/features/preprints/pages/landing/preprints-landing.component.scss b/src/app/features/preprints/pages/landing/preprints-landing.component.scss new file mode 100644 index 000000000..59c5ab2a5 --- /dev/null +++ b/src/app/features/preprints/pages/landing/preprints-landing.component.scss @@ -0,0 +1,14 @@ +@use "assets/styles/mixins" as mix; +@use "assets/styles/variables" as var; + +.subheader { + color: var.$dark-blue-1; + + .provider-description { + line-height: mix.rem(24px); + } +} + +.blue-dark-gradient { + background: url("/assets/images/dark-blue-gradient.png") center; +} diff --git a/src/app/features/preprints/pages/landing/preprints-landing.component.spec.ts b/src/app/features/preprints/pages/landing/preprints-landing.component.spec.ts new file mode 100644 index 000000000..749079411 --- /dev/null +++ b/src/app/features/preprints/pages/landing/preprints-landing.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PreprintsLandingComponent } from './preprints-landing.component'; + +describe('OverviewComponent', () => { + let component: PreprintsLandingComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PreprintsLandingComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(PreprintsLandingComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/preprints/pages/landing/preprints-landing.component.ts b/src/app/features/preprints/pages/landing/preprints-landing.component.ts new file mode 100644 index 000000000..4e07ba713 --- /dev/null +++ b/src/app/features/preprints/pages/landing/preprints-landing.component.ts @@ -0,0 +1,83 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +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 { Router, RouterLink } from '@angular/router'; + +import { + AdvisoryBoardComponent, + BrowseBySubjectsComponent, + PreprintServicesComponent, +} from '@osf/features/preprints/components'; +import { BrandService } from '@osf/features/preprints/services'; +import { + GetHighlightedSubjectsByProviderId, + GetPreprintProviderById, + GetPreprintProvidersToAdvertise, + PreprintsSelectors, +} from '@osf/features/preprints/store'; +import { SearchInputComponent } from '@shared/components'; +import { ResourceTab } from '@shared/enums'; + +@Component({ + selector: 'osf-overview', + imports: [ + Button, + SearchInputComponent, + RouterLink, + AdvisoryBoardComponent, + PreprintServicesComponent, + BrowseBySubjectsComponent, + Skeleton, + TranslateModule, + ], + templateUrl: './preprints-landing.component.html', + styleUrl: './preprints-landing.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PreprintsLandingComponent implements OnInit { + @HostBinding('class') classes = 'flex-1 flex flex-column w-full h-full'; + private readonly router = inject(Router); + private readonly OSF_PROVIDER_ID = 'osf'; + private readonly actions = createDispatchMap({ + getPreprintProviderById: GetPreprintProviderById, + getPreprintProvidersToAdvertise: GetPreprintProvidersToAdvertise, + getHighlightedSubjectsByProviderId: GetHighlightedSubjectsByProviderId, + }); + + osfPreprintProvider = select(PreprintsSelectors.getPreprintProviderDetails); + isPreprintProviderLoading = select(PreprintsSelectors.isPreprintProviderDetailsLoading); + preprintProvidersToAdvertise = select(PreprintsSelectors.getPreprintProvidersToAdvertise); + highlightedSubjectsByProviderId = select(PreprintsSelectors.getHighlightedSubjectsForProvider); + areSubjectsLoading = select(PreprintsSelectors.areSubjectsLoading); + + addPreprint() { + // [RNi] TODO: Implement the logic to add a preprint. + } + + constructor() { + effect(() => { + const provider = this.osfPreprintProvider(); + + if (provider) { + BrandService.applyBranding(provider.brand); + } + }); + } + + ngOnInit(): void { + this.actions.getPreprintProviderById(this.OSF_PROVIDER_ID); + this.actions.getPreprintProvidersToAdvertise(); + this.actions.getHighlightedSubjectsByProviderId(this.OSF_PROVIDER_ID); + } + + redirectToSearchPageWithValue(searchValue: string) { + this.router.navigate(['/search'], { + queryParams: { search: searchValue, resourceTab: ResourceTab.Preprints }, + }); + } +} diff --git a/src/app/features/preprints/preprints.component.html b/src/app/features/preprints/preprints.component.html new file mode 100644 index 000000000..f58acf390 --- /dev/null +++ b/src/app/features/preprints/preprints.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/app/features/preprints/preprints.component.scss b/src/app/features/preprints/preprints.component.scss new file mode 100644 index 000000000..f28af4ffb --- /dev/null +++ b/src/app/features/preprints/preprints.component.scss @@ -0,0 +1,7 @@ +@use "assets/styles/mixins" as mix; + +.desktop { + @include mix.flex-column; + flex: 1; + margin-top: mix.rem(70px); +} diff --git a/src/app/features/preprints/preprints.component.spec.ts b/src/app/features/preprints/preprints.component.spec.ts new file mode 100644 index 000000000..9936528a8 --- /dev/null +++ b/src/app/features/preprints/preprints.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PreprintsComponent } from './preprints.component'; + +describe('PreprintsComponent', () => { + let component: PreprintsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PreprintsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(PreprintsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/preprints/preprints.component.ts b/src/app/features/preprints/preprints.component.ts new file mode 100644 index 000000000..90508c6f7 --- /dev/null +++ b/src/app/features/preprints/preprints.component.ts @@ -0,0 +1,17 @@ +import { ChangeDetectionStrategy, Component, HostBinding, inject } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { RouterOutlet } from '@angular/router'; + +import { IS_WEB } from '@shared/utils'; + +@Component({ + selector: 'osf-preprints', + imports: [RouterOutlet], + templateUrl: './preprints.component.html', + styleUrl: './preprints.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PreprintsComponent { + protected readonly isDesktop = toSignal(inject(IS_WEB)); + @HostBinding('class') classes = 'flex flex-1 flex-column w-full h-full'; +} diff --git a/src/app/features/preprints/services/brand.service.ts b/src/app/features/preprints/services/brand.service.ts new file mode 100644 index 000000000..ece0b5c72 --- /dev/null +++ b/src/app/features/preprints/services/brand.service.ts @@ -0,0 +1,13 @@ +import { Brand } from '@osf/features/preprints/models'; + +export class BrandService { + static applyBranding(brand: Brand): void { + const root = document.documentElement; + + root.style.setProperty('--preprints-branding-primary-color', brand.primaryColor); + 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})`); + } +} diff --git a/src/app/features/preprints/services/index.ts b/src/app/features/preprints/services/index.ts new file mode 100644 index 000000000..fd2d1dc28 --- /dev/null +++ b/src/app/features/preprints/services/index.ts @@ -0,0 +1,2 @@ +export { BrandService } from './brand.service'; +export { PreprintsService } from './preprints.service'; diff --git a/src/app/features/preprints/services/preprints.service.ts b/src/app/features/preprints/services/preprints.service.ts new file mode 100644 index 000000000..6dd185f05 --- /dev/null +++ b/src/app/features/preprints/services/preprints.service.ts @@ -0,0 +1,58 @@ +import { map, Observable } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { JsonApiService } from '@core/services'; +import { JsonApiResponse } from '@osf/core/models'; +import { PreprintsMapper } from '@osf/features/preprints/mappers'; +import { + PreprintProviderDetails, + PreprintProviderDetailsGetResponse, + PreprintProviderToAdvertise, + Subject, + SubjectGetResponse, +} from '@osf/features/preprints/models'; + +import { environment } from 'src/environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class PreprintsService { + jsonApiService = inject(JsonApiService); + baseUrl = `${environment.apiUrl}/providers/preprints/`; + + getPreprintProviderById(id: string): Observable { + return this.jsonApiService + .get>(`${this.baseUrl}${id}/?embed=brand`) + .pipe( + map((response) => { + return PreprintsMapper.fromPreprintProviderDetailsGetResponse(response.data); + }) + ); + } + + getPreprintProvidersToAdvertise(): Observable { + return this.jsonApiService + .get< + JsonApiResponse + >(`${this.baseUrl}?filter[advertise_on_discover_page]=true&reload=true`) + .pipe( + map((response) => { + return PreprintsMapper.fromPreprintProvidersToAdvertiseGetResponse(response.data); + }) + ); + } + + getHighlightedSubjectsByProviderId(providerId: string): Observable { + return this.jsonApiService + .get< + JsonApiResponse + >(`${this.baseUrl}${providerId}/subjects/highlighted/?page[size]=20`) + .pipe( + map((response) => { + return PreprintsMapper.fromSubjectsGetResponse(response.data); + }) + ); + } +} diff --git a/src/app/features/preprints/store/index.ts b/src/app/features/preprints/store/index.ts new file mode 100644 index 000000000..d1f1d4f1a --- /dev/null +++ b/src/app/features/preprints/store/index.ts @@ -0,0 +1,4 @@ +export * from './preprints.actions'; +export * from './preprints.model'; +export * from './preprints.selectors'; +export * from './preprints.state'; diff --git a/src/app/features/preprints/store/preprints.actions.ts b/src/app/features/preprints/store/preprints.actions.ts new file mode 100644 index 000000000..5fc7c95fb --- /dev/null +++ b/src/app/features/preprints/store/preprints.actions.ts @@ -0,0 +1,15 @@ +export class GetPreprintProviderById { + static readonly type = '[Preprints] Get Provider By Id'; + + constructor(public id: string) {} +} + +export class GetHighlightedSubjectsByProviderId { + static readonly type = '[Preprints] Get Highlighted Subjects By Provider Id'; + + constructor(public providerId: string) {} +} + +export class GetPreprintProvidersToAdvertise { + static readonly type = '[Preprints] Get Preprint Providers To Advertise'; +} diff --git a/src/app/features/preprints/store/preprints.model.ts b/src/app/features/preprints/store/preprints.model.ts new file mode 100644 index 000000000..45a150f0d --- /dev/null +++ b/src/app/features/preprints/store/preprints.model.ts @@ -0,0 +1,8 @@ +import { PreprintProviderDetails, PreprintProviderToAdvertise, Subject } from '@osf/features/preprints/models'; +import { AsyncStateModel } from '@shared/models'; + +export interface PreprintsStateModel { + preprintProviderDetails: 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 new file mode 100644 index 000000000..b2e0bcee9 --- /dev/null +++ b/src/app/features/preprints/store/preprints.selectors.ts @@ -0,0 +1,30 @@ +import { Selector } from '@ngxs/store'; + +import { PreprintsState, PreprintsStateModel } from '@osf/features/preprints/store'; + +export class PreprintsSelectors { + @Selector([PreprintsState]) + static getPreprintProviderDetails(state: PreprintsStateModel) { + return state.preprintProviderDetails.data; + } + + @Selector([PreprintsState]) + static isPreprintProviderDetailsLoading(state: PreprintsStateModel) { + return state.preprintProviderDetails.isLoading; + } + + @Selector([PreprintsState]) + static getPreprintProvidersToAdvertise(state: PreprintsStateModel) { + return state.preprintProvidersToAdvertise.data; + } + + @Selector([PreprintsState]) + static getHighlightedSubjectsForProvider(state: PreprintsStateModel) { + return state.highlightedSubjectsForProvider.data; + } + + @Selector([PreprintsState]) + static areSubjectsLoading(state: PreprintsStateModel) { + return state.highlightedSubjectsForProvider.isLoading; + } +} diff --git a/src/app/features/preprints/store/preprints.state.ts b/src/app/features/preprints/store/preprints.state.ts new file mode 100644 index 000000000..deaef2f5c --- /dev/null +++ b/src/app/features/preprints/store/preprints.state.ts @@ -0,0 +1,111 @@ +import { Action, State, StateContext } from '@ngxs/store'; +import { patch } from '@ngxs/store/operators'; + +import { tap, throwError } from 'rxjs'; +import { catchError } from 'rxjs/operators'; + +import { inject, Injectable } from '@angular/core'; + +import { PreprintsService } from '@osf/features/preprints/services'; +import { + GetHighlightedSubjectsByProviderId, + GetPreprintProviderById, + GetPreprintProvidersToAdvertise, +} from '@osf/features/preprints/store/preprints.actions'; +import { PreprintsStateModel } from '@osf/features/preprints/store/preprints.model'; + +@State({ + name: 'preprints', + defaults: { + preprintProviderDetails: { + data: null, + isLoading: false, + error: null, + }, + preprintProvidersToAdvertise: { + data: [], + isLoading: false, + error: null, + }, + highlightedSubjectsForProvider: { + data: [], + isLoading: false, + error: null, + }, + }, +}) +@Injectable() +export class PreprintsState { + #preprintsService = inject(PreprintsService); + + @Action(GetPreprintProviderById) + getPreprintProviderById(ctx: StateContext, action: GetPreprintProviderById) { + ctx.setState(patch({ preprintProviderDetails: patch({ isLoading: true }) })); + + return this.#preprintsService.getPreprintProviderById(action.id).pipe( + tap((data) => { + ctx.setState( + patch({ + preprintProviderDetails: patch({ + data: data, + isLoading: false, + }), + }) + ); + }), + catchError((error) => this.handleError(ctx, 'preprintProviderDetails', error)) + ); + } + + @Action(GetPreprintProvidersToAdvertise) + getPreprintProvidersToAdvertise(ctx: StateContext) { + ctx.setState(patch({ preprintProvidersToAdvertise: patch({ isLoading: true }) })); + + return this.#preprintsService.getPreprintProvidersToAdvertise().pipe( + tap((data) => { + ctx.setState( + patch({ + preprintProvidersToAdvertise: patch({ + data: data, + isLoading: false, + }), + }) + ); + }), + catchError((error) => this.handleError(ctx, 'preprintProvidersToAdvertise', error)) + ); + } + + @Action(GetHighlightedSubjectsByProviderId) + getHighlightedSubjectsByProviderId( + ctx: StateContext, + action: GetHighlightedSubjectsByProviderId + ) { + ctx.setState(patch({ highlightedSubjectsForProvider: patch({ isLoading: true }) })); + + return this.#preprintsService.getHighlightedSubjectsByProviderId(action.providerId).pipe( + tap((data) => { + ctx.setState( + patch({ + highlightedSubjectsForProvider: patch({ + data: data, + isLoading: false, + }), + }) + ); + }), + catchError((error) => this.handleError(ctx, 'highlightedSubjectsForProvider', error)) + ); + } + + private handleError(ctx: StateContext, section: keyof PreprintsStateModel, error: Error) { + ctx.patchState({ + [section]: { + ...ctx.getState()[section], + isLoading: false, + error: error.message, + }, + }); + return throwError(() => error); + } +} diff --git a/src/app/shared/components/search-input/search-input.component.html b/src/app/shared/components/search-input/search-input.component.html index d1c3a99a5..5a03c7daa 100644 --- a/src/app/shared/components/search-input/search-input.component.html +++ b/src/app/shared/components/search-input/search-input.component.html @@ -7,5 +7,6 @@ pInputText [ngModel]="searchValue()" (ngModelChange)="onSearchChange($event)" + (keydown.enter)="onEnterClicked()" /> diff --git a/src/app/shared/components/search-input/search-input.component.ts b/src/app/shared/components/search-input/search-input.component.ts index 87b6af8b4..441ac1b11 100644 --- a/src/app/shared/components/search-input/search-input.component.ts +++ b/src/app/shared/components/search-input/search-input.component.ts @@ -1,6 +1,6 @@ import { InputText } from 'primeng/inputtext'; -import { ChangeDetectionStrategy, Component, input, model } from '@angular/core'; +import { ChangeDetectionStrategy, Component, input, model, output } from '@angular/core'; import { FormsModule } from '@angular/forms'; @Component({ @@ -13,8 +13,20 @@ import { FormsModule } from '@angular/forms'; export class SearchInputComponent { placeholder = input(''); searchValue = model(); + triggerSearch = output(); onSearchChange(value: string): void { this.searchValue.set(value); } + + onEnterClicked() { + const searchValue = this.searchValue(); + if (!searchValue) { + return; + } + + if (searchValue.trim().length > 0) { + this.triggerSearch.emit(searchValue); + } + } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 9b3e7d2d9..e88f0c087 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -11,6 +11,7 @@ "support": "Support", "meetings": "Meetings", "myProjects": "My Projects", + "preprints": "Preprints", "donate": "Donate", "profileSettings": "Profile Settings", "accountSettings": "Account Settings", @@ -937,6 +938,29 @@ "downloadButton": "Download" } }, + "preprints": { + "title": "Preprints", + "addPreprint": "Add a Preprint", + "poweredBy": "Powered by OSF Preprints", + "searchPlaceholder": "Search Preprints", + "showExample": "Show an example", + "browseBySubjects": { + "title": "Browse By Subjects" + }, + "services": { + "title": "Preprint Services", + "description": "Leading preprint service providers use this open source infrastructure to support their communities." + }, + "createServer": { + "title": "Create your own branded preprint servers backed by the OSF?", + "description": "Check out the", + "openSourceCode": "open source code", + "and": "and", + "publicRoadmap": "public roadmap", + "inputWelcome": "Input welcome!", + "contactUs": "Contact us" + } + }, "truncatedText": { "readMore": "Read more", "hide": "Hide" diff --git a/src/assets/images/auth-background.png b/src/assets/images/dark-blue-gradient.png similarity index 100% rename from src/assets/images/auth-background.png rename to src/assets/images/dark-blue-gradient.png diff --git a/src/assets/images/preprints/preprints-services-background.png b/src/assets/images/preprints/preprints-services-background.png new file mode 100644 index 000000000..69860ac7f Binary files /dev/null and b/src/assets/images/preprints/preprints-services-background.png differ diff --git a/src/assets/styles/_variables.scss b/src/assets/styles/_variables.scss index 175152e55..3a9b57174 100644 --- a/src/assets/styles/_variables.scss +++ b/src/assets/styles/_variables.scss @@ -130,4 +130,10 @@ $white-60: var(--white-60); ), #f3f8fd; --gradient-3: linear-gradient(90.12deg, #dbedfb 0.03%, #eff4fc 54.38%, #dbedfb 98.07%); + + --preprints-branding-primary-color: var(--pr-blue-1); + --preprints-branding-secondary-color: var(--pr-blue-1); + --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 new file mode 100644 index 000000000..85fae76d4 --- /dev/null +++ b/src/assets/styles/components/advisory-board.scss @@ -0,0 +1,17 @@ +@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/overrides/button.scss b/src/assets/styles/overrides/button.scss index 6d1d6ffa0..c4d3a7838 100644 --- a/src/assets/styles/overrides/button.scss +++ b/src/assets/styles/overrides/button.scss @@ -200,6 +200,13 @@ } } +.p-button-link { + a { + color: var.$white; + text-decoration: none; + } +} + .p-button-link:hover { .p-button-label { text-decoration: none; @@ -243,11 +250,13 @@ display: flex; flex-direction: column; justify-content: end; + .p-button { background: transparent; color: var.$dark-blue-2; padding: 0; } + i { color: var.$grey-1; } diff --git a/src/assets/styles/styles.scss b/src/assets/styles/styles.scss index 3a373383d..f0f7d706d 100644 --- a/src/assets/styles/styles.scss +++ b/src/assets/styles/styles.scss @@ -37,3 +37,4 @@ @use "./overrides/tooltip"; @use "./overrides/toast"; @use "./overrides/primeflex-override"; +@use "./components/advisory-board";