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' }} |
+
+
+
+
+
+
+
+

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

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

+
{{ 'meetings.landing.features.enhance.title' | translate }}
+
{{ 'meetings.landing.features.enhance.description' | translate }}
+
+
+
+
+
+ {{ 'meetings.landing.users.title' | translate }}
+
+
+
+
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 @@