diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index c7b266930..970c35867 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -56,6 +56,27 @@ export const routes: Routes = [ loadComponent: () => import('./features/privacy-policy/privacy-policy.component').then((mod) => mod.PrivacyPolicyComponent), }, + { + path: 'meetings', + loadComponent: () => import('./features/meetings/meetings.component').then((mod) => mod.MeetingsComponent), + children: [ + { + path: '', + pathMatch: 'full', + loadComponent: () => + import('@osf/features/meetings/pages/meetings-landing/meetings-landing.component').then( + (mod) => mod.MeetingsLandingComponent + ), + }, + { + path: ':id', + loadComponent: () => + import('@osf/features/meetings/pages/meeting-details/meeting-details.component').then( + (mod) => mod.MeetingDetailsComponent + ), + }, + ], + }, { path: 'my-projects', loadComponent: () => diff --git a/src/app/core/constants/nav-items.constant.ts b/src/app/core/constants/nav-items.constant.ts index 661a1e514..51be08e56 100644 --- a/src/app/core/constants/nav-items.constant.ts +++ b/src/app/core/constants/nav-items.constant.ts @@ -27,6 +27,12 @@ export const NAV_ITEMS: NavItem[] = [ icon: 'my-projects', useExactMatch: true, }, + { + path: '/meetings', + label: 'navigation.meetings', + icon: 'meetings', + useExactMatch: true, + }, { path: '/settings', label: 'navigation.settings', diff --git a/src/app/features/meetings/constants/index.ts b/src/app/features/meetings/constants/index.ts new file mode 100644 index 000000000..667e4e510 --- /dev/null +++ b/src/app/features/meetings/constants/index.ts @@ -0,0 +1,2 @@ +export * from './meeting-submissions-table.constants'; +export * from './meetings-table.constants'; diff --git a/src/app/features/meetings/constants/meeting-submissions-table.constants.ts b/src/app/features/meetings/constants/meeting-submissions-table.constants.ts new file mode 100644 index 000000000..a15615151 --- /dev/null +++ b/src/app/features/meetings/constants/meeting-submissions-table.constants.ts @@ -0,0 +1,12 @@ +import { TableParameters } from '@shared/entities/table-parameters.interface'; + +export const MEETING_SUBMISSIONS_TABLE_PARAMS: TableParameters = { + rows: 10, + paginator: true, + scrollable: false, + rowsPerPageOptions: [5, 10, 25], + totalRecords: 3, + firstRowIndex: 0, + defaultSortColumn: null, + defaultSortOrder: null, +}; diff --git a/src/app/features/meetings/constants/meetings-table.constants.ts b/src/app/features/meetings/constants/meetings-table.constants.ts new file mode 100644 index 000000000..b2acea4b8 --- /dev/null +++ b/src/app/features/meetings/constants/meetings-table.constants.ts @@ -0,0 +1,12 @@ +import { TableParameters } from '@shared/entities/table-parameters.interface'; + +export const MEETINGS_TABLE_PARAMS: TableParameters = { + rows: 10, + paginator: true, + scrollable: false, + rowsPerPageOptions: [5, 10, 25], + totalRecords: 3, + firstRowIndex: 0, + defaultSortColumn: null, + defaultSortOrder: null, +}; diff --git a/src/app/features/meetings/meetings.component.html b/src/app/features/meetings/meetings.component.html new file mode 100644 index 000000000..f58acf390 --- /dev/null +++ b/src/app/features/meetings/meetings.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/app/features/meetings/meetings.component.scss b/src/app/features/meetings/meetings.component.scss new file mode 100644 index 000000000..4ac9ed819 --- /dev/null +++ b/src/app/features/meetings/meetings.component.scss @@ -0,0 +1,7 @@ +@use "assets/styles/mixins" as mix; + +.desktop { + @include mix.flex-column; + flex: 1; + margin-top: 4.5rem; +} diff --git a/src/app/features/meetings/meetings.component.spec.ts b/src/app/features/meetings/meetings.component.spec.ts new file mode 100644 index 000000000..b51d5ddfb --- /dev/null +++ b/src/app/features/meetings/meetings.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MeetingsComponent } from './meetings.component'; + +describe('MeetingsComponent', () => { + let component: MeetingsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MeetingsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(MeetingsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/meetings/meetings.component.ts b/src/app/features/meetings/meetings.component.ts new file mode 100644 index 000000000..1963fae85 --- /dev/null +++ b/src/app/features/meetings/meetings.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/breakpoints.tokens'; + +@Component({ + selector: 'osf-meetings', + imports: [RouterOutlet], + templateUrl: './meetings.component.html', + styleUrl: './meetings.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MeetingsComponent { + 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/meetings/models/index.ts b/src/app/features/meetings/models/index.ts new file mode 100644 index 000000000..781399be8 --- /dev/null +++ b/src/app/features/meetings/models/index.ts @@ -0,0 +1 @@ +export * from './meetings.models'; diff --git a/src/app/features/meetings/models/meetings.models.ts b/src/app/features/meetings/models/meetings.models.ts new file mode 100644 index 000000000..20962bc75 --- /dev/null +++ b/src/app/features/meetings/models/meetings.models.ts @@ -0,0 +1,18 @@ +export interface Meeting { + id: string; + title: string; + submissionsCount: number; + location: string; + startDate: Date; + endDate: Date; +} + +export interface MeetingSubmission { + id: string; + title: string; + dateCreated: Date; + authorName: string; + downloadCount: number; + meetingCategory: string; + downloadLink: string; +} diff --git a/src/app/features/meetings/pages/index.ts b/src/app/features/meetings/pages/index.ts new file mode 100644 index 000000000..2af54e93d --- /dev/null +++ b/src/app/features/meetings/pages/index.ts @@ -0,0 +1,2 @@ +export * from './meetings-details'; +export * from './meetings-landing'; diff --git a/src/app/features/meetings/pages/meeting-details/meeting-details.component.html b/src/app/features/meetings/pages/meeting-details/meeting-details.component.html new file mode 100644 index 000000000..cfa0e861f --- /dev/null +++ b/src/app/features/meetings/pages/meeting-details/meeting-details.component.html @@ -0,0 +1,70 @@ + + +
+ + + + + + + {{ 'meetings.details.table.columns.title' | translate }} + + + + {{ 'meetings.details.table.columns.author' | translate }} + + + + {{ 'meetings.details.table.columns.category' | translate }} + + + + {{ 'meetings.details.table.columns.dateCreated' | translate }} + + + + {{ 'meetings.details.table.columns.downloads' | translate }} + + + + + + + {{ item.title }} + {{ item.authorName }} + {{ item.meetingCategory }} + {{ item.dateCreated | date: 'MMM d, y, h:mm a' }} + + + {{ item.downloadCount }} + + + + +
diff --git a/src/app/features/meetings/pages/meeting-details/meeting-details.component.scss b/src/app/features/meetings/pages/meeting-details/meeting-details.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/meetings/pages/meeting-details/meeting-details.component.spec.ts b/src/app/features/meetings/pages/meeting-details/meeting-details.component.spec.ts new file mode 100644 index 000000000..d84d035a9 --- /dev/null +++ b/src/app/features/meetings/pages/meeting-details/meeting-details.component.spec.ts @@ -0,0 +1,26 @@ +import { TranslateModule } from '@ngx-translate/core'; +import { MockModule } from 'ng-mocks'; + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterModule } from '@angular/router'; + +import { MeetingDetailsComponent } from './meeting-details.component'; + +describe('MeetingDetailsComponent', () => { + let component: MeetingDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MeetingDetailsComponent, MockModule(TranslateModule), MockModule(RouterModule)], + }).compileComponents(); + + fixture = TestBed.createComponent(MeetingDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/meetings/pages/meeting-details/meeting-details.component.ts b/src/app/features/meetings/pages/meeting-details/meeting-details.component.ts new file mode 100644 index 000000000..70b94e9fe --- /dev/null +++ b/src/app/features/meetings/pages/meeting-details/meeting-details.component.ts @@ -0,0 +1,59 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { SortEvent } from 'primeng/api'; +import { Button } from 'primeng/button'; +import { TableModule, TablePageEvent } from 'primeng/table'; + +import { DatePipe } from '@angular/common'; +import { ChangeDetectionStrategy, Component, computed, HostBinding, inject, signal } from '@angular/core'; +import { RouterLink } from '@angular/router'; + +import { MEETING_SUBMISSIONS_TABLE_PARAMS } from '@osf/features/meetings/constants'; +import { Meeting, MeetingSubmission } from '@osf/features/meetings/models'; +import { testMeeting, testSubmissions } from '@osf/features/meetings/test-data'; +import { SearchInputComponent } from '@shared/components/search-input/search-input.component'; +import { SubHeaderComponent } from '@shared/components/sub-header/sub-header.component'; +import { TableParameters } from '@shared/entities/table-parameters.interface'; + +@Component({ + selector: 'osf-meeting-details', + imports: [SubHeaderComponent, SearchInputComponent, DatePipe, TableModule, Button, RouterLink, TranslatePipe], + templateUrl: './meeting-details.component.html', + styleUrl: './meeting-details.component.scss', + providers: [DatePipe], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MeetingDetailsComponent { + @HostBinding('class') classes = 'flex-1 flex flex-column w-full h-full'; + #datePipe = inject(DatePipe); + searchValue = signal(''); + sortColumn = signal(undefined); + tableParams = signal({ + ...MEETING_SUBMISSIONS_TABLE_PARAMS, + firstRowIndex: 0, + }); + meeting = signal(testMeeting); + meetingSubmissions = signal(testSubmissions); + + pageDescription = computed(() => { + if (!this.meeting) { + return ''; + } + + return `${this.meeting().location} | ${this.#datePipe.transform(this.meeting().startDate, 'MMM d, y')} + - ${this.#datePipe.transform(this.meeting().endDate, 'MMM d, y')}`; + }); + + onPageChange(tablePageEvent: TablePageEvent) { + // [RNi] TODO: implement paging logic and handle event while integrating API + } + + onSort(sortEvent: SortEvent) { + // [RNi] TODO: implement sorting logic and handle event while integrating API + } + + downloadSubmission(event: Event, item: MeetingSubmission) { + event.stopPropagation(); + window.open(item.downloadLink, '_blank'); + } +} diff --git a/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.html b/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.html new file mode 100644 index 000000000..bbd1b04da --- /dev/null +++ b/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.html @@ -0,0 +1,113 @@ + + +
+
+

{{ 'meetings.landing.submissionsNote' | translate }}

+
+ + + + + + + + {{ 'meetings.landing.table.columns.title' | translate }} + + + + {{ 'meetings.landing.table.columns.submissions' | translate }} + + + + {{ 'meetings.landing.table.columns.location' | translate }} + + + + {{ 'meetings.landing.table.columns.date' | translate }} + + + + + + + {{ item.title }} + {{ item.submissionsCount }} + {{ item.location }} + {{ item.startDate | date: 'MMM d, y, h:mm a' }} + + + + +
+ +
+ Discover +

{{ 'meetings.landing.features.discover.title' | translate }}

+

{{ 'meetings.landing.features.discover.description' | translate }}

+
+
+ + +
+ Share +

{{ 'meetings.landing.features.share.title' | translate }}

+

{{ 'meetings.landing.features.share.description' | translate }}

+
+
+ + +
+ Enhance +

{{ 'meetings.landing.features.enhance.title' | translate }}

+

{{ 'meetings.landing.features.enhance.description' | translate }}

+
+
+
+ +
+

{{ 'meetings.landing.users.title' | translate }}

+ +
+ APS + BITSS + NRAO + SPSP +
+
+
diff --git a/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.scss b/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.spec.ts b/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.spec.ts new file mode 100644 index 000000000..7c13d0ea3 --- /dev/null +++ b/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.spec.ts @@ -0,0 +1,26 @@ +import { TranslateModule } from '@ngx-translate/core'; +import { MockModule } from 'ng-mocks'; + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterModule } from '@angular/router'; + +import { MeetingsLandingComponent } from './meetings-landing.component'; + +describe('MeetingsLandingComponent', () => { + let component: MeetingsLandingComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MeetingsLandingComponent, MockModule(TranslateModule), MockModule(RouterModule)], + }).compileComponents(); + + fixture = TestBed.createComponent(MeetingsLandingComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.ts b/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.ts new file mode 100644 index 000000000..8e46b7aa0 --- /dev/null +++ b/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.ts @@ -0,0 +1,51 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { SortEvent } from 'primeng/api'; +import { Card } from 'primeng/card'; +import { TableModule, TablePageEvent } from 'primeng/table'; + +import { DatePipe } from '@angular/common'; +import { ChangeDetectionStrategy, Component, HostBinding, inject, signal } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { Router } from '@angular/router'; + +import { MEETINGS_TABLE_PARAMS } from '@osf/features/meetings/constants'; +import { Meeting } from '@osf/features/meetings/models'; +import { testMeetings } from '@osf/features/meetings/test-data'; +import { SearchInputComponent } from '@shared/components/search-input/search-input.component'; +import { SubHeaderComponent } from '@shared/components/sub-header/sub-header.component'; +import { TableParameters } from '@shared/entities/table-parameters.interface'; +import { IS_XSMALL } from '@shared/utils/breakpoints.tokens'; + +@Component({ + selector: 'osf-meetings-landing', + imports: [SubHeaderComponent, Card, SearchInputComponent, DatePipe, TableModule, TranslatePipe], + templateUrl: './meetings-landing.component.html', + styleUrl: './meetings-landing.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MeetingsLandingComponent { + @HostBinding('class') classes = 'flex-1 flex flex-column w-full h-full'; + readonly isXSmall = toSignal(inject(IS_XSMALL)); + #router = inject(Router); + + searchValue = signal(''); + sortColumn = signal(undefined); + tableParams = signal({ + ...MEETINGS_TABLE_PARAMS, + firstRowIndex: 0, + }); + meetings = signal(testMeetings); + + onPageChange(tablePageEvent: TablePageEvent) { + // [RNi] TODO: implement paging logic and handle event while integrating API + } + + onSort(sortEvent: SortEvent) { + // [RNi] TODO: implement sorting logic and handle event while integrating API + } + + navigateToMeeting(meeting: Meeting): void { + this.#router.navigate(['/meetings', meeting.id]); + } +} diff --git a/src/app/features/meetings/test-data.ts b/src/app/features/meetings/test-data.ts new file mode 100644 index 000000000..c2af0eb12 --- /dev/null +++ b/src/app/features/meetings/test-data.ts @@ -0,0 +1,56 @@ +export const testMeetings = [ + { + id: '123', + title: 'Lorem ipsum dolor sit amet', + submissionsCount: 18, + location: 'Virtual', + startDate: new Date(), + endDate: new Date(), + }, + { + id: '124', + title: 'Lorem ipsum dolor sit amet', + submissionsCount: 10, + location: 'Virtual', + startDate: new Date(), + endDate: new Date(), + }, + { + id: '125', + title: 'Lorem ipsum dolor sit amet', + submissionsCount: 24, + location: 'Irving, Texas', + startDate: new Date(), + endDate: new Date(), + }, +]; + +export const testMeeting = { + id: '123', + title: 'Society for Personality and Social Psychology 2016', + submissionsCount: 5, + location: 'San Diego, CA', + startDate: new Date(), + endDate: new Date(), +}; + +export const testSubmissions = [ + { + id: '123', + title: 'Lorem ipsum dolor sit amet', + dateCreated: new Date('2020-06-03T21:36:05.241346Z'), + authorName: 'John Doe', + downloadCount: 0, + meetingCategory: 'poster', + downloadLink: 'https://osf.io/download/72wkh/', + }, + { + id: '123', + title: 'Lorem ipsum dolor sit amet', + dateCreated: new Date('2018-06-03T19:36:05.241346Z'), + authorName: 'John Doe', + downloadCount: 6, + meetingCategory: 'poster', + downloadLink: 'https://osf.io/download/72wkh/', + }, +]; diff --git a/src/app/shared/components/sub-header/sub-header.component.html b/src/app/shared/components/sub-header/sub-header.component.html index 339563fd8..ceaf9c205 100644 --- a/src/app/shared/components/sub-header/sub-header.component.html +++ b/src/app/shared/components/sub-header/sub-header.component.html @@ -1,13 +1,20 @@
-
- @if (icon()) { - +
+
+ @if (icon()) { + + } +

{{ title() }}

+
+ @if (showButton()) { +
+ +
} -

{{ title() }}

- @if (showButton()) { -
- -
+ @if (description()) { +
+

{{ description() }}

+
}
diff --git a/src/app/shared/components/sub-header/sub-header.component.scss b/src/app/shared/components/sub-header/sub-header.component.scss index 7a7be5443..1d82afac2 100644 --- a/src/app/shared/components/sub-header/sub-header.component.scss +++ b/src/app/shared/components/sub-header/sub-header.component.scss @@ -5,32 +5,40 @@ width: 100%; .sub-header { - @include mix.flex-align-center; - gap: 0.7rem; + @include mix.flex-column; + gap: 1.75rem; padding: 2.5rem 1.7rem 3rem 1.7rem; width: 100%; + color: var.$dark-blue-1; - .title-icon { - display: flex; - align-items: center; - column-gap: 0.8rem; - } + .title-row { + @include mix.flex-align-center; + gap: 0.7rem; - i { - font-size: 2.6rem; - color: var.$dark-blue-1; - } + &.mobile { + @include mix.flex-column; + align-items: start; + row-gap: 2.14rem; + } + + .title-icon { + display: flex; + align-items: center; + column-gap: 0.8rem; - .btn-container { - margin-left: auto; + i { + font-size: 2.6rem; + color: var.$dark-blue-1; + } + } + + .btn-container { + margin-left: auto; + } } - } - .mobile { - display: flex; - flex-direction: column; - align-items: start; - row-gap: 2.14rem; - padding: 2.5rem 1.1rem 2.5rem 1.1rem; + &.mobile { + padding: 2.5rem 1.1rem 2.5rem 1.1rem; + } } } diff --git a/src/app/shared/components/sub-header/sub-header.component.ts b/src/app/shared/components/sub-header/sub-header.component.ts index a7bb0c5f4..81ae282f2 100644 --- a/src/app/shared/components/sub-header/sub-header.component.ts +++ b/src/app/shared/components/sub-header/sub-header.component.ts @@ -17,6 +17,7 @@ export class SubHeaderComponent { buttonLabel = input(''); title = input(''); icon = input(''); + description = input(''); buttonClick = output(); #isXSmall$ = inject(IS_XSMALL); isXSmall = toSignal(this.#isXSmall$); diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index ba7f1fbf3..c4bd8d748 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -8,6 +8,7 @@ "home": "Home", "searchOsf": "Search OSF", "support": "Support", + "meetings": "Meetings", "myProjects": "My Projects", "donate": "Donate", "profileSettings": "Profile Settings", @@ -624,6 +625,52 @@ } } }, + "meetings": { + "landing": { + "title": "Meetings", + "description": "A free poster and presentation sharing service for academic meetings and conferences", + "submissionsNote": "Only conferences with at least five submissions are displayed.", + "searchPlaceholder": "Search meetings", + "table": { + "columns": { + "title": "Title", + "submissions": "Submissions", + "location": "Location", + "date": "Date" + } + }, + "features": { + "discover": { + "title": "Discover", + "description": "Explore posters and presentations from events long after they're over." + }, + "share": { + "title": "Share", + "description": "Get persistent links to your content and increase your impact." + }, + "enhance": { + "title": "Enhance", + "description": "Add supplementary data and materials to your submission to make your work more transparent." + } + }, + "users": { + "title": "Who uses OSF Meetings?" + } + }, + "details": { + "searchPlaceholder": "Search", + "table": { + "columns": { + "title": "Title", + "author": "Author", + "category": "Category", + "dateCreated": "Date Created", + "downloads": "Downloads" + } + }, + "downloadButton": "Download" + } + }, "footer": { "links": { "centerForOpenScience": "Center for Open Science", @@ -643,4 +690,4 @@ }, "copyright": "Copyright © 2011-2025" } -} \ No newline at end of file +} diff --git a/src/assets/icons/colored/aps.svg b/src/assets/icons/colored/aps.svg new file mode 100644 index 000000000..2515f9b0f --- /dev/null +++ b/src/assets/icons/colored/aps.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/icons/colored/bitss.svg b/src/assets/icons/colored/bitss.svg new file mode 100644 index 000000000..b4e7a8f54 --- /dev/null +++ b/src/assets/icons/colored/bitss.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/icons/colored/conference-participants.svg b/src/assets/icons/colored/conference-participants.svg new file mode 100644 index 000000000..003a42771 --- /dev/null +++ b/src/assets/icons/colored/conference-participants.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/icons/colored/nrao.svg b/src/assets/icons/colored/nrao.svg new file mode 100644 index 000000000..426b39fc2 --- /dev/null +++ b/src/assets/icons/colored/nrao.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/icons/colored/spsp.svg b/src/assets/icons/colored/spsp.svg new file mode 100644 index 000000000..2af1a7360 --- /dev/null +++ b/src/assets/icons/colored/spsp.svg @@ -0,0 +1,9 @@ + + + + + + + + +