diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index e81a97da2..1330d34ee 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -204,6 +204,10 @@ export const routes: Routes = [ import('./core/components/request-access/request-access.component').then((mod) => mod.RequestAccessComponent), data: { skipBreadcrumbs: true }, }, + { + path: 'registries', + loadChildren: () => import('./features/registries/registries.routes').then((mod) => mod.registriesRoutes), + }, { path: '**', loadComponent: () => diff --git a/src/app/core/constants/nav-items.constant.ts b/src/app/core/constants/nav-items.constant.ts index 960ffa864..4d5984963 100644 --- a/src/app/core/constants/nav-items.constant.ts +++ b/src/app/core/constants/nav-items.constant.ts @@ -28,6 +28,20 @@ export const NAV_ITEMS: NavItem[] = [ icon: 'my-projects', useExactMatch: true, }, + { + path: '/registries', + label: 'navigation.registries', + icon: 'registries', + isCollapsible: true, + useExactMatch: false, + items: [ + { + path: '/registries', + label: 'navigation.registriesSubRoutes.overview', + useExactMatch: false, + }, + ], + }, { path: '/preprints', label: 'navigation.preprints', diff --git a/src/app/features/registries/components/index.ts b/src/app/features/registries/components/index.ts new file mode 100644 index 000000000..91c733b6d --- /dev/null +++ b/src/app/features/registries/components/index.ts @@ -0,0 +1 @@ +export * from './registry-services/registry-services.component'; diff --git a/src/app/features/registries/components/registry-services/registry-services.component.html b/src/app/features/registries/components/registry-services/registry-services.component.html new file mode 100644 index 000000000..5a629b5e4 --- /dev/null +++ b/src/app/features/registries/components/registry-services/registry-services.component.html @@ -0,0 +1,26 @@ +
+

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

+

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

+ +
+ @for (registryService of registryServices; track $index) { + + } +
+ +
+ +
+
diff --git a/src/app/features/registries/components/registry-services/registry-services.component.scss b/src/app/features/registries/components/registry-services/registry-services.component.scss new file mode 100644 index 000000000..8378698b2 --- /dev/null +++ b/src/app/features/registries/components/registry-services/registry-services.component.scss @@ -0,0 +1,18 @@ +@use "assets/styles/mixins" as mix; +@use "assets/styles/variables" as var; + +.services-background-image { + background: url("/assets/images/preprints/preprints-services-background.png") center; +} + +.registry-service-item { + min-width: mix.rem(312px); + min-height: mix.rem(120px); + background-size: contain; + background-repeat: no-repeat; + background-position: center; +} + +.contact-us-button { + --p-button-padding-x: 2rem; +} diff --git a/src/app/features/registries/components/registry-services/registry-services.component.spec.ts b/src/app/features/registries/components/registry-services/registry-services.component.spec.ts new file mode 100644 index 000000000..eaec061b9 --- /dev/null +++ b/src/app/features/registries/components/registry-services/registry-services.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RegistryServicesComponent } from './registry-services.component'; + +describe('RegistryServicesComponent', () => { + let component: RegistryServicesComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RegistryServicesComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(RegistryServicesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/registries/components/registry-services/registry-services.component.ts b/src/app/features/registries/components/registry-services/registry-services.component.ts new file mode 100644 index 000000000..85dba0605 --- /dev/null +++ b/src/app/features/registries/components/registry-services/registry-services.component.ts @@ -0,0 +1,23 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { Button } from 'primeng/button'; + +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; + +import { RegistryServiceIcons } from '@shared/constants'; + +@Component({ + selector: 'osf-registry-services', + imports: [TranslatePipe, RouterLink, Button], + templateUrl: './registry-services.component.html', + styleUrl: './registry-services.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RegistryServicesComponent { + protected registryServices = RegistryServiceIcons; + + openEmail() { + window.location.href = 'mailto:contact@osf.io'; + } +} diff --git a/src/app/features/registries/pages/index.ts b/src/app/features/registries/pages/index.ts new file mode 100644 index 000000000..d5bc067cd --- /dev/null +++ b/src/app/features/registries/pages/index.ts @@ -0,0 +1 @@ +export * from './registries-landing/registries-landing.component'; 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 new file mode 100644 index 000000000..475cedd14 --- /dev/null +++ b/src/app/features/registries/pages/registries-landing/registries-landing.component.html @@ -0,0 +1,49 @@ +
+
+ + + +
+ + + +
+
+

{{ 'registries.browse' | translate }}

+
+ + @if (!isRegistriesLoading()) { + @for (item of registries(); track item.id) { + + } + } @else { + + } + + +
+
diff --git a/src/app/features/registries/pages/registries-landing/registries-landing.component.scss b/src/app/features/registries/pages/registries-landing/registries-landing.component.scss new file mode 100644 index 000000000..39af4152d --- /dev/null +++ b/src/app/features/registries/pages/registries-landing/registries-landing.component.scss @@ -0,0 +1,9 @@ +@use "assets/styles/variables" as var; + +.subheader { + color: var.$dark-blue-1; +} + +.registries { + background: var.$white; +} diff --git a/src/app/features/registries/pages/registries-landing/registries-landing.component.spec.ts b/src/app/features/registries/pages/registries-landing/registries-landing.component.spec.ts new file mode 100644 index 000000000..64450d266 --- /dev/null +++ b/src/app/features/registries/pages/registries-landing/registries-landing.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RegistriesLandingComponent } from './registries-landing.component'; + +describe('RegistriesLandingComponent', () => { + let component: RegistriesLandingComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RegistriesLandingComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(RegistriesLandingComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/registries/pages/registries-landing/registries-landing.component.ts b/src/app/features/registries/pages/registries-landing/registries-landing.component.ts new file mode 100644 index 000000000..5cfefc8d5 --- /dev/null +++ b/src/app/features/registries/pages/registries-landing/registries-landing.component.ts @@ -0,0 +1,65 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { TranslatePipe } from '@ngx-translate/core'; + +import { Button } from 'primeng/button'; + +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { Router } from '@angular/router'; + +import { RegistryServicesComponent } from '@osf/features/registries/components'; +import { GetRegistries, RegistriesSelectors } from '@osf/features/registries/store'; +import { + LoadingSpinnerComponent, + ResourceCardComponent, + SearchInputComponent, + SubHeaderComponent, +} from '@shared/components'; +import { ResourceTab } from '@shared/enums'; + +@Component({ + selector: 'osf-registries-landing', + imports: [ + Button, + TranslatePipe, + SearchInputComponent, + RegistryServicesComponent, + ResourceCardComponent, + LoadingSpinnerComponent, + SubHeaderComponent, + ], + templateUrl: './registries-landing.component.html', + styleUrl: './registries-landing.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RegistriesLandingComponent implements OnInit { + private router = inject(Router); + + protected searchControl = new FormControl(''); + + private readonly actions = createDispatchMap({ + getRegistries: GetRegistries, + }); + + protected registries = select(RegistriesSelectors.getRegistries); + protected isRegistriesLoading = select(RegistriesSelectors.isRegistriesLoading); + + ngOnInit(): void { + this.actions.getRegistries(); + } + + redirectToSearchPageWithValue(): void { + const searchValue = this.searchControl.value; + + this.router.navigate(['/search'], { + queryParams: { search: searchValue, resourceTab: ResourceTab.Registrations }, + }); + } + + redirectToSearchPageRegistrations(): void { + this.router.navigate(['/search'], { + queryParams: { resourceTab: ResourceTab.Registrations }, + }); + } +} diff --git a/src/app/features/registries/registries.component.html b/src/app/features/registries/registries.component.html new file mode 100644 index 000000000..21c4dd5c7 --- /dev/null +++ b/src/app/features/registries/registries.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/app/features/registries/registries.component.scss b/src/app/features/registries/registries.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/registries/registries.component.spec.ts b/src/app/features/registries/registries.component.spec.ts new file mode 100644 index 000000000..01003ff5e --- /dev/null +++ b/src/app/features/registries/registries.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RegistriesComponent } from './registries.component'; + +describe('RegistriesComponent', () => { + let component: RegistriesComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RegistriesComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(RegistriesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/registries/registries.component.ts b/src/app/features/registries/registries.component.ts new file mode 100644 index 000000000..78716bee1 --- /dev/null +++ b/src/app/features/registries/registries.component.ts @@ -0,0 +1,11 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; + +@Component({ + selector: 'osf-registries', + imports: [RouterOutlet], + templateUrl: './registries.component.html', + styleUrl: './registries.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RegistriesComponent {} diff --git a/src/app/features/registries/registries.routes.ts b/src/app/features/registries/registries.routes.ts new file mode 100644 index 000000000..d410bfa4a --- /dev/null +++ b/src/app/features/registries/registries.routes.ts @@ -0,0 +1,25 @@ +import { provideStates } from '@ngxs/store'; + +import { Routes } from '@angular/router'; + +import { RegistriesComponent } from '@osf/features/registries/registries.component'; +import { RegistriesState } from '@osf/features/registries/store'; + +export const registriesRoutes: Routes = [ + { + path: '', + component: RegistriesComponent, + providers: [provideStates([RegistriesState])], + children: [ + { + path: '', + pathMatch: 'full', + redirectTo: 'overview', + }, + { + path: 'overview', + loadComponent: () => import('@osf/features/registries/pages').then((c) => c.RegistriesLandingComponent), + }, + ], + }, +]; diff --git a/src/app/features/registries/store/index.ts b/src/app/features/registries/store/index.ts new file mode 100644 index 000000000..92a9455c1 --- /dev/null +++ b/src/app/features/registries/store/index.ts @@ -0,0 +1,4 @@ +export * from './registries.actions'; +export * from './registries.model'; +export * from './registries.selectors'; +export * from './registries.state'; diff --git a/src/app/features/registries/store/registries.actions.ts b/src/app/features/registries/store/registries.actions.ts new file mode 100644 index 000000000..91edcdf7f --- /dev/null +++ b/src/app/features/registries/store/registries.actions.ts @@ -0,0 +1,3 @@ +export class GetRegistries { + static readonly type = '[Registries] Get Registries'; +} diff --git a/src/app/features/registries/store/registries.model.ts b/src/app/features/registries/store/registries.model.ts new file mode 100644 index 000000000..42de4eea6 --- /dev/null +++ b/src/app/features/registries/store/registries.model.ts @@ -0,0 +1,5 @@ +import { AsyncStateModel, Resource } from '@shared/models'; + +export interface RegistriesStateModel { + registries: AsyncStateModel; +} diff --git a/src/app/features/registries/store/registries.selectors.ts b/src/app/features/registries/store/registries.selectors.ts new file mode 100644 index 000000000..4a3812a4e --- /dev/null +++ b/src/app/features/registries/store/registries.selectors.ts @@ -0,0 +1,17 @@ +import { Selector } from '@ngxs/store'; + +import { RegistriesStateModel } from '@osf/features/registries/store/registries.model'; +import { RegistriesState } from '@osf/features/registries/store/registries.state'; +import { Resource } from '@shared/models'; + +export class RegistriesSelectors { + @Selector([RegistriesState]) + static getRegistries(state: RegistriesStateModel): Resource[] { + return state.registries.data; + } + + @Selector([RegistriesState]) + static isRegistriesLoading(state: RegistriesStateModel): boolean { + return state.registries.isLoading; + } +} diff --git a/src/app/features/registries/store/registries.state.ts b/src/app/features/registries/store/registries.state.ts new file mode 100644 index 000000000..f6b384660 --- /dev/null +++ b/src/app/features/registries/store/registries.state.ts @@ -0,0 +1,65 @@ +import { Action, State, StateContext } from '@ngxs/store'; + +import { tap, throwError } from 'rxjs'; +import { catchError } from 'rxjs/operators'; + +import { inject, Injectable } from '@angular/core'; + +import { ResourceTab } from '@shared/enums'; +import { SearchService } from '@shared/services'; +import { getResourceTypes } from '@shared/utils'; + +import { GetRegistries } from './registries.actions'; +import { RegistriesStateModel } from './registries.model'; + +@Injectable() +@State({ + name: 'registries', + defaults: { + registries: { + data: [], + isLoading: false, + error: null, + }, + }, +}) +export class RegistriesState { + searchService = inject(SearchService); + + @Action(GetRegistries) + getRegistries(ctx: StateContext) { + const state = ctx.getState(); + ctx.patchState({ + registries: { + ...state.registries, + isLoading: true, + }, + }); + + const resourceType = getResourceTypes(ResourceTab.Registrations); + + return this.searchService.getResources({}, '', '', resourceType).pipe( + tap((registries) => { + ctx.patchState({ + registries: { + data: registries.resources, + isLoading: false, + error: null, + }, + }); + }), + catchError((error) => this.handleError(ctx, 'registries', error)) + ); + } + + private handleError(ctx: StateContext, section: 'registries', error: Error) { + ctx.patchState({ + [section]: { + ...ctx.getState()[section], + isLoading: false, + error: error.message, + }, + }); + return throwError(() => error); + } +} diff --git a/src/app/shared/constants/index.ts b/src/app/shared/constants/index.ts index fd9e0a180..5118515c0 100644 --- a/src/app/shared/constants/index.ts +++ b/src/app/shared/constants/index.ts @@ -4,6 +4,7 @@ export * from './addons-tab-options.const'; export * from './input-limits.const'; export * from './input-validation-messages.const'; export * from './meetings-table.constants'; +export * from './registry-services-icons.const'; export * from './remove-nullable.const'; export * from './resource-filters-defaults'; export * from './resource-languages.const'; diff --git a/src/app/shared/constants/registry-services-icons.const.ts b/src/app/shared/constants/registry-services-icons.const.ts new file mode 100644 index 000000000..5b43f6813 --- /dev/null +++ b/src/app/shared/constants/registry-services-icons.const.ts @@ -0,0 +1,38 @@ +export const RegistryServiceIcons = [ + { + src: 'assets/images/registries-services/ASIST_logo.png', + id: 'darpaasist', + }, + { + src: 'assets/images/registries-services/CharacterLab_logo.png', + id: 'characterlabregistry', + }, + { + src: 'assets/images/registries-services/dam_logo.png', + id: 'dam', + }, + { + src: 'assets/images/registries-services/EGAP_white_logo.png', + id: 'egap', + }, + { + src: 'assets/images/registries-services/Metascience_logo.png', + id: 'metascience', + }, + { + src: 'assets/images/registries-services/RWE_logo.png', + id: 'rwe', + }, + { + src: 'assets/images/registries-services/YOUth_logo.png', + id: 'youthstudy', + }, + { + src: 'assets/images/registries-services/TWF_Consciousness_Logo.png', + id: 'twcfconscious', + }, + { + src: 'assets/images/registries-services/GFS_Logo.png', + id: 'gfs', + }, +]; diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 92fc1e669..410977fee 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -55,6 +55,10 @@ "support": "Support", "meetings": "Meetings", "myProjects": "My Projects", + "registries": "Registries", + "registriesSubRoutes": { + "overview": "Overview" + }, "preprints": "Preprints", "preprintsSubRoutes": { "overview": "Overview", @@ -1382,6 +1386,19 @@ "providerLogoImageAlt": "Provider Logo" } }, + "registries": { + "title": "Registries", + "addRegistration": "Add a Registration", + "description": "The open registries network", + "searchPlaceholder": "Search Registrations", + "services": { + "title": "Registry Services", + "description": "Leading registry service providers use this open source infrastructure to support their communities.", + "contactUs": "Contact Us" + }, + "browse": "Browse Registrations", + "seeMore": "See more" + }, "truncatedText": { "readMore": "Read more", "hide": "Hide" diff --git a/src/assets/images/registries-services/ASIST_logo.png b/src/assets/images/registries-services/ASIST_logo.png new file mode 100644 index 000000000..fbede4264 Binary files /dev/null and b/src/assets/images/registries-services/ASIST_logo.png differ diff --git a/src/assets/images/registries-services/CharacterLab_logo.png b/src/assets/images/registries-services/CharacterLab_logo.png new file mode 100644 index 000000000..396eff573 Binary files /dev/null and b/src/assets/images/registries-services/CharacterLab_logo.png differ diff --git a/src/assets/images/registries-services/EGAP_white_logo.png b/src/assets/images/registries-services/EGAP_white_logo.png new file mode 100644 index 000000000..a3d1ed5a8 Binary files /dev/null and b/src/assets/images/registries-services/EGAP_white_logo.png differ diff --git a/src/assets/images/registries-services/GFS_Logo.png b/src/assets/images/registries-services/GFS_Logo.png new file mode 100644 index 000000000..9664d7666 Binary files /dev/null and b/src/assets/images/registries-services/GFS_Logo.png differ diff --git a/src/assets/images/registries-services/Metascience_logo.png b/src/assets/images/registries-services/Metascience_logo.png new file mode 100644 index 000000000..1ffce2f5b Binary files /dev/null and b/src/assets/images/registries-services/Metascience_logo.png differ diff --git a/src/assets/images/registries-services/RWE_logo.png b/src/assets/images/registries-services/RWE_logo.png new file mode 100644 index 000000000..8854811db Binary files /dev/null and b/src/assets/images/registries-services/RWE_logo.png differ diff --git a/src/assets/images/registries-services/TWF_Consciousness_Logo.png b/src/assets/images/registries-services/TWF_Consciousness_Logo.png new file mode 100644 index 000000000..adacc753b Binary files /dev/null and b/src/assets/images/registries-services/TWF_Consciousness_Logo.png differ diff --git a/src/assets/images/registries-services/YOUth_logo.png b/src/assets/images/registries-services/YOUth_logo.png new file mode 100644 index 000000000..246264b7a Binary files /dev/null and b/src/assets/images/registries-services/YOUth_logo.png differ diff --git a/src/assets/images/registries-services/dam_logo.png b/src/assets/images/registries-services/dam_logo.png new file mode 100644 index 000000000..21f2e58f1 Binary files /dev/null and b/src/assets/images/registries-services/dam_logo.png differ diff --git a/src/assets/images/registries-services/prereg.png b/src/assets/images/registries-services/prereg.png new file mode 100644 index 000000000..8237b43bd Binary files /dev/null and b/src/assets/images/registries-services/prereg.png differ