diff --git a/.github/workflows/quality-check.yml b/.github/workflows/quality-check.yml index f14b6b2e..cd99bd6c 100644 --- a/.github/workflows/quality-check.yml +++ b/.github/workflows/quality-check.yml @@ -3,10 +3,10 @@ name: Quality Checks +# DISABLED: Moving away from old UI - quality checks no longer needed +# Changed to manual trigger only - will not run automatically on PRs on: - pull_request: - branches: [main] - types: [opened, synchronize, reopened] + workflow_dispatch: jobs: quality-checks: @@ -23,8 +23,8 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '22' - cache: 'yarn' + node-version: "22" + cache: "yarn" - name: Install dependencies run: yarn install --immutable @@ -46,9 +46,9 @@ jobs: needs: quality-checks uses: ./.github/workflows/e2e-tests.yml with: - node-version: '22' - test-command: 'e2e' - browser: 'chromium' # Use only Chromium for PR testing for faster feedback + node-version: "22" + test-command: "e2e" + browser: "chromium" # Use only Chromium for PR testing for faster feedback skip-build: false secrets: TEST_USERNAME: ${{ secrets.TEST_USERNAME }} diff --git a/apps/lfx-one/src/app/app.routes.ts b/apps/lfx-one/src/app/app.routes.ts index f7c40baa..924b761a 100644 --- a/apps/lfx-one/src/app/app.routes.ts +++ b/apps/lfx-one/src/app/app.routes.ts @@ -8,8 +8,24 @@ import { authGuard } from './shared/guards/auth.guard'; export const routes: Routes = [ { path: '', - loadComponent: () => import('./modules/pages/home/home.component').then((m) => m.HomeComponent), canActivate: [authGuard], + loadComponent: () => import('./layouts/main-layout/main-layout.component').then((m) => m.MainLayoutComponent), + children: [ + { + path: '', + loadComponent: () => import('./modules/pages/dashboard/dashboard.component').then((m) => m.DashboardComponent), + }, + { + path: 'projects', + loadComponent: () => import('./modules/pages/home/home.component').then((m) => m.HomeComponent), + }, + ], + }, + // Old UI route - shows when "Old UI" persona is selected + { + path: 'old-ui', + canActivate: [authGuard], + loadComponent: () => import('./modules/pages/home/home.component').then((m) => m.HomeComponent), }, { path: 'meetings', diff --git a/apps/lfx-one/src/app/layouts/main-layout/main-layout.component.html b/apps/lfx-one/src/app/layouts/main-layout/main-layout.component.html new file mode 100644 index 00000000..d2c3c23e --- /dev/null +++ b/apps/lfx-one/src/app/layouts/main-layout/main-layout.component.html @@ -0,0 +1,36 @@ + + + +
+ + + + + @if (showMobileSidebar()) { +
+
+
+

Menu

+ +
+
+ +
+
+
+ } + + +
+ +
+
diff --git a/apps/lfx-one/src/app/layouts/main-layout/main-layout.component.scss b/apps/lfx-one/src/app/layouts/main-layout/main-layout.component.scss new file mode 100644 index 00000000..df18259a --- /dev/null +++ b/apps/lfx-one/src/app/layouts/main-layout/main-layout.component.scss @@ -0,0 +1,6 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +:host { + display: block; +} diff --git a/apps/lfx-one/src/app/layouts/main-layout/main-layout.component.ts b/apps/lfx-one/src/app/layouts/main-layout/main-layout.component.ts new file mode 100644 index 00000000..a9a77c92 --- /dev/null +++ b/apps/lfx-one/src/app/layouts/main-layout/main-layout.component.ts @@ -0,0 +1,106 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +import { CommonModule } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { NavigationEnd, Router, RouterModule } from '@angular/router'; +import { AppService } from '@app/shared/services/app.service'; +import { SidebarComponent } from '@components/sidebar/sidebar.component'; +import { SidebarMenuItem } from '@lfx-one/shared/interfaces'; +import { filter } from 'rxjs'; + +@Component({ + selector: 'lfx-main-layout', + standalone: true, + imports: [CommonModule, RouterModule, SidebarComponent], + templateUrl: './main-layout.component.html', + styleUrl: './main-layout.component.scss', +}) +export class MainLayoutComponent { + private readonly router = inject(Router); + private readonly appService = inject(AppService); + + // Expose mobile sidebar state from service + protected readonly showMobileSidebar = this.appService.showMobileSidebar; + + // Sidebar navigation items + protected readonly sidebarItems: SidebarMenuItem[] = [ + { + label: 'Home', + icon: 'fa-light fa-house', + routerLink: '/', + }, + { + label: 'My Meetings', + icon: 'fa-light fa-video', + routerLink: '/my-meetings', + disabled: true, + }, + { + label: 'Project Health', + icon: 'fa-light fa-heart-pulse', + routerLink: '/project-health', + disabled: true, + }, + { + label: 'Events & Community', + icon: 'fa-light fa-calendar', + routerLink: '/events-community', + disabled: true, + }, + { + label: 'Training & Certification', + icon: 'fa-light fa-book-open', + routerLink: '/training-certification', + disabled: true, + }, + ]; + + // Sidebar footer items + protected readonly sidebarFooterItems: SidebarMenuItem[] = [ + { + label: 'Documentation', + icon: 'fa-light fa-file-lines', + url: 'https://docs.lfx.linuxfoundation.org', + }, + { + label: 'Submit a Ticket', + icon: 'fa-light fa-circle-question', + url: 'https://jira.linuxfoundation.org/plugins/servlet/theme/portal/4', + }, + { + label: 'Changelog', + icon: 'fa-light fa-rectangle-history', + routerLink: '/changelog', + disabled: true, + }, + { + label: 'Settings', + icon: 'fa-light fa-gear', + routerLink: '/settings', + disabled: true, + }, + { + label: 'Profile', + icon: 'fa-light fa-user', + routerLink: '/profile', + }, + ]; + + public constructor() { + // Close mobile sidebar on navigation + this.router.events + .pipe( + filter((event) => event instanceof NavigationEnd), + takeUntilDestroyed() + ) + .subscribe(() => { + this.appService.closeMobileSidebar(); + }); + } + + public closeMobileSidebar(): void { + this.appService.closeMobileSidebar(); + } +} diff --git a/apps/lfx-one/src/app/layouts/profile-layout/profile-layout.component.ts b/apps/lfx-one/src/app/layouts/profile-layout/profile-layout.component.ts index 0d543a55..ca42a491 100644 --- a/apps/lfx-one/src/app/layouts/profile-layout/profile-layout.component.ts +++ b/apps/lfx-one/src/app/layouts/profile-layout/profile-layout.component.ts @@ -145,7 +145,7 @@ export class ProfileLayoutComponent { const profile = this.profile(); if (!profile?.profile) return ''; - return profile.profile.job_title || ''; + return profile.profile.title || ''; }); } diff --git a/apps/lfx-one/src/app/modules/pages/dashboard/components/my-meetings/my-meetings.component.html b/apps/lfx-one/src/app/modules/pages/dashboard/components/my-meetings/my-meetings.component.html new file mode 100644 index 00000000..02521b3a --- /dev/null +++ b/apps/lfx-one/src/app/modules/pages/dashboard/components/my-meetings/my-meetings.component.html @@ -0,0 +1,62 @@ + + + +
+ +
+

My Meetings

+ +
+ + +
+
+ @if (todayMeetings().length > 0 || upcomingMeetings().length > 0) { + + @if (todayMeetings().length > 0) { +
+

Today

+
+ @for (item of todayMeetings(); track item.meeting.uid) { + + } +
+
+ } + + + @if (upcomingMeetings().length > 0) { +
+

Upcoming

+
+ @for (item of upcomingMeetings(); track item.meeting.uid) { + + } +
+
+ } + } @else { + +
+ No meetings scheduled +
+ } +
+
+
diff --git a/apps/lfx-one/src/app/modules/pages/dashboard/components/my-meetings/my-meetings.component.scss b/apps/lfx-one/src/app/modules/pages/dashboard/components/my-meetings/my-meetings.component.scss new file mode 100644 index 00000000..c32094b4 --- /dev/null +++ b/apps/lfx-one/src/app/modules/pages/dashboard/components/my-meetings/my-meetings.component.scss @@ -0,0 +1,2 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT diff --git a/apps/lfx-one/src/app/modules/pages/dashboard/components/my-meetings/my-meetings.component.ts b/apps/lfx-one/src/app/modules/pages/dashboard/components/my-meetings/my-meetings.component.ts new file mode 100644 index 00000000..9c92c272 --- /dev/null +++ b/apps/lfx-one/src/app/modules/pages/dashboard/components/my-meetings/my-meetings.component.ts @@ -0,0 +1,141 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +import { CommonModule } from '@angular/common'; +import { Component, computed, inject } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { Router } from '@angular/router'; +import { MeetingService } from '@app/shared/services/meeting.service'; +import { ButtonComponent } from '@components/button/button.component'; +import { DashboardMeetingCardComponent } from '@components/dashboard-meeting-card/dashboard-meeting-card.component'; + +import type { Meeting, MeetingOccurrence } from '@lfx-one/shared/interfaces'; + +interface MeetingWithOccurrence { + meeting: Meeting; + occurrence: MeetingOccurrence; + sortTime: number; +} + +@Component({ + selector: 'lfx-my-meetings', + standalone: true, + imports: [CommonModule, DashboardMeetingCardComponent, ButtonComponent], + templateUrl: './my-meetings.component.html', + styleUrl: './my-meetings.component.scss', +}) +export class MyMeetingsComponent { + private readonly meetingService = inject(MeetingService); + private readonly router = inject(Router); + private readonly allMeetings = toSignal(this.meetingService.getMeetings(), { initialValue: [] }); + + protected readonly todayMeetings = computed(() => { + const now = new Date(); + const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const todayEnd = new Date(today.getTime() + 24 * 60 * 60 * 1000); + const currentTime = now.getTime(); + const buffer = 40 * 60 * 1000; // 40 minutes in milliseconds + + const meetings: MeetingWithOccurrence[] = []; + + for (const meeting of this.allMeetings()) { + // Process occurrences if they exist + if (meeting.occurrences && meeting.occurrences.length > 0) { + for (const occurrence of meeting.occurrences) { + const startTime = new Date(occurrence.start_time); + const startTimeMs = startTime.getTime(); + const endTime = startTimeMs + occurrence.duration * 60 * 1000 + buffer; + + // Include if meeting is today and hasn't ended yet (including buffer) + if (startTime >= today && startTime < todayEnd && endTime >= currentTime) { + meetings.push({ + meeting, + occurrence, + sortTime: startTimeMs, + }); + } + } + } else { + // Handle meetings without occurrences (single meetings) + const startTime = new Date(meeting.start_time); + const startTimeMs = startTime.getTime(); + const endTime = startTimeMs + meeting.duration * 60 * 1000 + buffer; + + // Include if meeting is today and hasn't ended yet (including buffer) + if (startTime >= today && startTime < todayEnd && endTime >= currentTime) { + meetings.push({ + meeting, + occurrence: { + occurrence_id: '', + title: meeting.title, + description: meeting.description, + start_time: meeting.start_time, + duration: meeting.duration, + }, + sortTime: startTimeMs, + }); + } + } + } + + // Sort by earliest time first + return meetings.sort((a, b) => a.sortTime - b.sortTime); + }); + + protected readonly upcomingMeetings = computed(() => { + const now = new Date(); + const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const todayEnd = new Date(today.getTime() + 24 * 60 * 60 * 1000); + + const meetings: MeetingWithOccurrence[] = []; + + for (const meeting of this.allMeetings()) { + // Process occurrences if they exist + if (meeting.occurrences && meeting.occurrences.length > 0) { + for (const occurrence of meeting.occurrences) { + const startTime = new Date(occurrence.start_time); + const startTimeMs = startTime.getTime(); + + // Include if meeting is after today + if (startTime >= todayEnd) { + meetings.push({ + meeting, + occurrence, + sortTime: startTimeMs, + }); + } + } + } else { + // Handle meetings without occurrences (single meetings) + const startTime = new Date(meeting.start_time); + const startTimeMs = startTime.getTime(); + + // Include if meeting is after today + if (startTime >= todayEnd) { + meetings.push({ + meeting, + occurrence: { + occurrence_id: '', + title: meeting.title, + description: meeting.description, + start_time: meeting.start_time, + duration: meeting.duration, + }, + sortTime: startTimeMs, + }); + } + } + } + + // Sort by earliest time first and limit to 5 + return meetings.sort((a, b) => a.sortTime - b.sortTime).slice(0, 5); + }); + + public handleSeeMeeting(meetingId: string): void { + this.router.navigate(['/meetings', meetingId]); + } + + public handleViewAll(): void { + this.router.navigate(['/meetings']); + } +} diff --git a/apps/lfx-one/src/app/modules/pages/dashboard/components/my-projects/my-projects.component.html b/apps/lfx-one/src/app/modules/pages/dashboard/components/my-projects/my-projects.component.html new file mode 100644 index 00000000..43de6589 --- /dev/null +++ b/apps/lfx-one/src/app/modules/pages/dashboard/components/my-projects/my-projects.component.html @@ -0,0 +1,127 @@ + + + +
+
+

My Projects

+
+ +
+
+ + + + Project + Affiliation(s) + +
+ Code Activities + +
+ + +
+ Non-Code Activities + +
+ + +
+ + + + + +
+
+ @if (project.logo) { + + } @else { +
+ +
+ } +
+
+
{{ project.name }}
+
{{ project.role }}
+
+
+ + + + +
{{ project.affiliations.join(', ') }}
+ + + + +
+ + +
+ + + + +
+ + +
+ + +
+
+
+
+
diff --git a/apps/lfx-one/src/app/modules/pages/dashboard/components/my-projects/my-projects.component.scss b/apps/lfx-one/src/app/modules/pages/dashboard/components/my-projects/my-projects.component.scss new file mode 100644 index 00000000..c32094b4 --- /dev/null +++ b/apps/lfx-one/src/app/modules/pages/dashboard/components/my-projects/my-projects.component.scss @@ -0,0 +1,2 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT diff --git a/apps/lfx-one/src/app/modules/pages/dashboard/components/my-projects/my-projects.component.ts b/apps/lfx-one/src/app/modules/pages/dashboard/components/my-projects/my-projects.component.ts new file mode 100644 index 00000000..45e2fd87 --- /dev/null +++ b/apps/lfx-one/src/app/modules/pages/dashboard/components/my-projects/my-projects.component.ts @@ -0,0 +1,57 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { ChartComponent } from '@components/chart/chart.component'; +import { TableComponent } from '@components/table/table.component'; + +import type { ProjectItem } from '@lfx-one/shared/interfaces'; + +@Component({ + selector: 'lfx-my-projects', + standalone: true, + imports: [CommonModule, ChartComponent, TableComponent], + templateUrl: './my-projects.component.html', + styleUrl: './my-projects.component.scss', +}) +export class MyProjectsComponent { + protected readonly projects: ProjectItem[] = [ + { + name: 'Kubernetes', + logo: 'https://avatars.githubusercontent.com/u/13455738?s=280&v=4', + role: 'Maintainer', + affiliations: ['CNCF', 'Google'], + codeActivities: [28, 32, 30, 35, 38, 40, 42], + nonCodeActivities: [8, 10, 12, 11, 13, 14, 15], + status: 'active', + }, + { + name: 'Linux Kernel', + logo: 'https://upload.wikimedia.org/wikipedia/commons/3/35/Tux.svg', + role: 'Contributor', + affiliations: ['Linux Foundation'], + codeActivities: [15, 18, 20, 22, 24, 26, 28], + nonCodeActivities: [3, 4, 5, 6, 7, 7, 8], + status: 'active', + }, + { + name: 'Node.js', + logo: 'https://nodejs.org/static/logos/nodejsHex.svg', + role: 'Reviewer', + affiliations: ['OpenJS Foundation'], + codeActivities: [18, 16, 15, 14, 13, 12, 12], + nonCodeActivities: [8, 7, 6, 6, 5, 5, 5], + status: 'archived', + }, + ]; + + /** + * Generates labels for chart based on data length + * @param length - Number of data points + * @returns Array of empty strings for chart labels + */ + protected generateLabels(length: number): string[] { + return Array.from({ length }, () => ''); + } +} diff --git a/apps/lfx-one/src/app/modules/pages/dashboard/components/pending-actions/pending-actions.component.html b/apps/lfx-one/src/app/modules/pages/dashboard/components/pending-actions/pending-actions.component.html new file mode 100644 index 00000000..893de263 --- /dev/null +++ b/apps/lfx-one/src/app/modules/pages/dashboard/components/pending-actions/pending-actions.component.html @@ -0,0 +1,88 @@ + + + +
+ +
+

Pending Actions

+ +
+ + +
+
+ @for (item of pendingActions; track item.text) { +
+ +
+
+
+ +
+ + {{ item.type }} + +
+ + {{ item.badge }} + +
+ + +
+

+ {{ item.text }} +

+
+ + + +
+ } +
+
+
diff --git a/apps/lfx-one/src/app/modules/pages/dashboard/components/pending-actions/pending-actions.component.scss b/apps/lfx-one/src/app/modules/pages/dashboard/components/pending-actions/pending-actions.component.scss new file mode 100644 index 00000000..c32094b4 --- /dev/null +++ b/apps/lfx-one/src/app/modules/pages/dashboard/components/pending-actions/pending-actions.component.scss @@ -0,0 +1,2 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT diff --git a/apps/lfx-one/src/app/modules/pages/dashboard/components/pending-actions/pending-actions.component.ts b/apps/lfx-one/src/app/modules/pages/dashboard/components/pending-actions/pending-actions.component.ts new file mode 100644 index 00000000..9b461721 --- /dev/null +++ b/apps/lfx-one/src/app/modules/pages/dashboard/components/pending-actions/pending-actions.component.ts @@ -0,0 +1,54 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +import { CommonModule } from '@angular/common'; +import { Component, output } from '@angular/core'; +import { ButtonComponent } from '@components/button/button.component'; +import type { PendingActionItem } from '@lfx-one/shared/interfaces'; + +@Component({ + selector: 'lfx-pending-actions', + standalone: true, + imports: [CommonModule, ButtonComponent], + templateUrl: './pending-actions.component.html', + styleUrl: './pending-actions.component.scss', +}) +export class PendingActionsComponent { + public readonly actionClick = output(); + public readonly viewAll = output(); + + protected readonly pendingActions: PendingActionItem[] = [ + { + type: 'Issue', + badge: 'Kubernetes', + text: 'Maintainer tagged you for clarification on issue #238: Pod Autoscaler UI.', + icon: 'fa-light fa-envelope', + color: 'amber', + buttonText: 'Add Comment', + }, + { + type: 'PR Review', + badge: 'Linux Kernel', + text: 'Code review requested for pull request #456: Memory management optimization.', + icon: 'fa-light fa-code-pull-request', + color: 'blue', + buttonText: 'Review PR', + }, + { + type: 'Meeting', + badge: 'CNCF', + text: 'Technical Steering Committee meeting agenda review needed by EOD.', + icon: 'fa-light fa-calendar', + color: 'green', + buttonText: 'View Agenda', + }, + ]; + + public handleViewAll(): void { + this.viewAll.emit(); + } + + protected handleActionClick(item: PendingActionItem): void { + this.actionClick.emit(item); + } +} diff --git a/apps/lfx-one/src/app/modules/pages/dashboard/components/recent-progress/recent-progress.component.html b/apps/lfx-one/src/app/modules/pages/dashboard/components/recent-progress/recent-progress.component.html new file mode 100644 index 00000000..942ccdb9 --- /dev/null +++ b/apps/lfx-one/src/app/modules/pages/dashboard/components/recent-progress/recent-progress.component.html @@ -0,0 +1,63 @@ + + + +
+
+
+

Recent Progress

+
+ +
+ + + + +
+ + +
+
+
+ +
+
+ @for (item of progressItems; track item.label) { +
+
+
{{ item.label }}
+
+ +
+
+
{{ item.value }}
+ @if (item.subtitle) { +
{{ item.subtitle }}
+ } +
+
+
+ } +
+
+
diff --git a/apps/lfx-one/src/app/modules/pages/dashboard/components/recent-progress/recent-progress.component.scss b/apps/lfx-one/src/app/modules/pages/dashboard/components/recent-progress/recent-progress.component.scss new file mode 100644 index 00000000..121c9727 --- /dev/null +++ b/apps/lfx-one/src/app/modules/pages/dashboard/components/recent-progress/recent-progress.component.scss @@ -0,0 +1,11 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +.hide-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + + &::-webkit-scrollbar { + display: none; /* Chrome, Safari and Opera */ + } +} diff --git a/apps/lfx-one/src/app/modules/pages/dashboard/components/recent-progress/recent-progress.component.ts b/apps/lfx-one/src/app/modules/pages/dashboard/components/recent-progress/recent-progress.component.ts new file mode 100644 index 00000000..58202a55 --- /dev/null +++ b/apps/lfx-one/src/app/modules/pages/dashboard/components/recent-progress/recent-progress.component.ts @@ -0,0 +1,182 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +import { CommonModule } from '@angular/common'; +import { Component, ElementRef, ViewChild } from '@angular/core'; +import { ChartComponent } from '@components/chart/chart.component'; + +import type { ProgressItemWithChart } from '@lfx-one/shared/interfaces'; + +@Component({ + selector: 'lfx-recent-progress', + standalone: true, + imports: [CommonModule, ChartComponent], + templateUrl: './recent-progress.component.html', + styleUrl: './recent-progress.component.scss', +}) +export class RecentProgressComponent { + @ViewChild('progressScroll') protected progressScrollContainer!: ElementRef; + + protected readonly progressItems: ProgressItemWithChart[] = [ + { + label: 'Code Commits', + value: '47', + trend: 'up', + subtitle: 'Last 30 days', + chartData: { + labels: Array.from({ length: 30 }, (_, i) => `Day ${i + 1}`), + datasets: [ + { + data: Array.from({ length: 30 }, () => Math.floor(Math.random() * 6)), + borderColor: '#0094FF', + backgroundColor: 'rgba(0, 148, 255, 0.1)', + fill: true, + tension: 0.4, + borderWidth: 2, + pointRadius: 0, + }, + ], + }, + chartOptions: { + responsive: true, + maintainAspectRatio: false, + plugins: { legend: { display: false }, tooltip: { enabled: false } }, + scales: { + x: { display: false }, + y: { display: false }, + }, + }, + }, + { + label: 'Pull Requests Merged', + value: '12', + trend: 'up', + subtitle: 'Last 30 days', + chartData: { + labels: Array.from({ length: 30 }, (_, i) => `Day ${i + 1}`), + datasets: [ + { + data: Array.from({ length: 30 }, () => Math.floor(Math.random() * 3)), + borderColor: '#0094FF', + backgroundColor: 'rgba(0, 148, 255, 0.1)', + fill: true, + tension: 0.4, + borderWidth: 2, + pointRadius: 0, + }, + ], + }, + chartOptions: { + responsive: true, + maintainAspectRatio: false, + plugins: { legend: { display: false }, tooltip: { enabled: false } }, + scales: { + x: { display: false }, + y: { display: false }, + }, + }, + }, + { + label: 'Issues Resolved & Comments Added', + value: '34', + trend: 'up', + subtitle: 'Combined activity last 30 days', + chartData: { + labels: Array.from({ length: 30 }, (_, i) => `Day ${i + 1}`), + datasets: [ + { + data: Array.from({ length: 30 }, () => Math.floor(Math.random() * 5)), + borderColor: '#0094FF', + backgroundColor: 'rgba(0, 148, 255, 0.1)', + fill: true, + tension: 0.4, + borderWidth: 2, + pointRadius: 0, + }, + ], + }, + chartOptions: { + responsive: true, + maintainAspectRatio: false, + plugins: { legend: { display: false }, tooltip: { enabled: false } }, + scales: { + x: { display: false }, + y: { display: false }, + }, + }, + }, + { + label: 'Active Weeks Streak', + value: '12', + trend: 'up', + subtitle: 'Current streak', + chartData: { + labels: Array.from({ length: 20 }, (_, i) => `Week ${i + 1}`), + datasets: [ + { + data: Array.from({ length: 20 }, (_, i) => { + if (i >= 8) { + return 1; + } + return Math.random() > 0.5 ? 1 : 0; + }), + borderColor: '#10b981', + backgroundColor: 'rgba(16, 185, 129, 0.1)', + fill: true, + tension: 0.4, + borderWidth: 2, + pointRadius: 0, + }, + ], + }, + chartOptions: { + responsive: true, + maintainAspectRatio: false, + plugins: { legend: { display: false }, tooltip: { enabled: false } }, + scales: { + x: { display: false }, + y: { display: false }, + }, + }, + }, + { + label: 'Learning Hours', + value: '8.5', + trend: 'up', + subtitle: 'Last 30 days', + chartData: { + labels: Array.from({ length: 30 }, (_, i) => `Day ${i + 1}`), + datasets: [ + { + data: Array.from({ length: 30 }, () => Math.floor(Math.random() * 3)), + borderColor: '#93c5fd', + backgroundColor: 'rgba(147, 197, 253, 0.1)', + fill: true, + tension: 0.4, + borderWidth: 2, + pointRadius: 0, + }, + ], + }, + chartOptions: { + responsive: true, + maintainAspectRatio: false, + plugins: { legend: { display: false }, tooltip: { enabled: false } }, + scales: { + x: { display: false }, + y: { display: false }, + }, + }, + }, + ]; + + protected scrollLeft(): void { + const container = this.progressScrollContainer.nativeElement; + container.scrollBy({ left: -300, behavior: 'smooth' }); + } + + protected scrollRight(): void { + const container = this.progressScrollContainer.nativeElement; + container.scrollBy({ left: 300, behavior: 'smooth' }); + } +} diff --git a/apps/lfx-one/src/app/modules/pages/dashboard/dashboard.component.html b/apps/lfx-one/src/app/modules/pages/dashboard/dashboard.component.html new file mode 100644 index 00000000..61900393 --- /dev/null +++ b/apps/lfx-one/src/app/modules/pages/dashboard/dashboard.component.html @@ -0,0 +1,22 @@ + + + +
+ +
+ + + + +
+ + + + + +
+ + + +
+
diff --git a/apps/lfx-one/src/app/modules/pages/dashboard/dashboard.component.scss b/apps/lfx-one/src/app/modules/pages/dashboard/dashboard.component.scss new file mode 100644 index 00000000..c32094b4 --- /dev/null +++ b/apps/lfx-one/src/app/modules/pages/dashboard/dashboard.component.scss @@ -0,0 +1,2 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT diff --git a/apps/lfx-one/src/app/modules/pages/dashboard/dashboard.component.ts b/apps/lfx-one/src/app/modules/pages/dashboard/dashboard.component.ts new file mode 100644 index 00000000..c0976150 --- /dev/null +++ b/apps/lfx-one/src/app/modules/pages/dashboard/dashboard.component.ts @@ -0,0 +1,17 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +import { Component } from '@angular/core'; +import { MyMeetingsComponent } from './components/my-meetings/my-meetings.component'; +import { MyProjectsComponent } from './components/my-projects/my-projects.component'; +import { PendingActionsComponent } from './components/pending-actions/pending-actions.component'; +import { RecentProgressComponent } from './components/recent-progress/recent-progress.component'; + +@Component({ + selector: 'lfx-dashboard', + standalone: true, + imports: [RecentProgressComponent, PendingActionsComponent, MyMeetingsComponent, MyProjectsComponent], + templateUrl: './dashboard.component.html', + styleUrl: './dashboard.component.scss', +}) +export class DashboardComponent {} diff --git a/apps/lfx-one/src/app/modules/profile/edit/profile-edit.component.html b/apps/lfx-one/src/app/modules/profile/edit/profile-edit.component.html index 9a0cfd6b..50a9e576 100644 --- a/apps/lfx-one/src/app/modules/profile/edit/profile-edit.component.html +++ b/apps/lfx-one/src/app/modules/profile/edit/profile-edit.component.html @@ -97,7 +97,7 @@

= { given_name: formValue.given_name || undefined, family_name: formValue.family_name || undefined, - job_title: formValue.job_title || undefined, + title: formValue.title || undefined, organization: formValue.organization || undefined, country: formValue.country || undefined, state_province: formValue.state_province || undefined, @@ -224,7 +224,7 @@ export class ProfileEditComponent implements OnInit { given_name: profile.user.first_name || '', family_name: profile.user.last_name || '', username: profile.user.username || '', - job_title: profile.profile?.job_title || '', + title: profile.profile?.title || '', organization: profile.profile?.organization || '', country: countryValue, state_province: profile.profile?.state_province || '', diff --git a/apps/lfx-one/src/app/shared/components/dashboard-meeting-card/dashboard-meeting-card.component.html b/apps/lfx-one/src/app/shared/components/dashboard-meeting-card/dashboard-meeting-card.component.html new file mode 100644 index 00000000..80c548e2 --- /dev/null +++ b/apps/lfx-one/src/app/shared/components/dashboard-meeting-card/dashboard-meeting-card.component.html @@ -0,0 +1,114 @@ + + + + +
+
+ +
+
+ + {{ meetingTypeInfo().label }} + + @if (meeting().project_name) { + + {{ meeting().project_name }} + + } +
+
+ @if (hasYoutubeUploads()) { +
+ +
+ } + @if (hasRecording()) { +
+ +
+ } + @if (hasTranscripts()) { +
+ +
+ } + @if (hasAiSummary()) { +
+ +
+ } + @if (!isPrivate()) { +
+ +
+ } +
+
+ + +
+

+ {{ meetingTitle() }} +

+
+
+ +
+
+ +
+
+
+ + +
+
+
+ + {{ formattedTime() }} +
+ +
+ + + @if (isTodayMeeting()) { + + } +
+
+
diff --git a/apps/lfx-one/src/app/shared/components/dashboard-meeting-card/dashboard-meeting-card.component.ts b/apps/lfx-one/src/app/shared/components/dashboard-meeting-card/dashboard-meeting-card.component.ts new file mode 100644 index 00000000..689daea7 --- /dev/null +++ b/apps/lfx-one/src/app/shared/components/dashboard-meeting-card/dashboard-meeting-card.component.ts @@ -0,0 +1,142 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +import { CommonModule } from '@angular/common'; +import { Component, computed, input, output, Signal } from '@angular/core'; +import { ButtonComponent } from '@components/button/button.component'; +import { Meeting, MeetingOccurrence } from '@lfx-one/shared'; +import { TooltipModule } from 'primeng/tooltip'; + +interface MeetingTypeBadge { + label: string; + className: string; +} + +@Component({ + selector: 'lfx-dashboard-meeting-card', + standalone: true, + imports: [CommonModule, ButtonComponent, TooltipModule], + templateUrl: './dashboard-meeting-card.component.html', +}) +export class DashboardMeetingCardComponent { + public readonly meeting = input.required(); + public readonly occurrence = input(null); + public readonly onSeeMeeting = output(); + + // Computed values + public readonly meetingTypeInfo: Signal = computed(() => { + const type = this.meeting().meeting_type?.toLowerCase(); + + switch (type) { + case 'technical': + return { label: 'Technical', className: 'bg-purple-100 text-purple-600' }; + case 'maintainers': + return { label: 'Maintainers', className: 'bg-blue-100 text-blue-600' }; + case 'board': + return { label: 'Board', className: 'bg-red-100 text-red-600' }; + case 'marketing': + return { label: 'Marketing', className: 'bg-green-100 text-green-600' }; + case 'legal': + return { label: 'Legal', className: 'bg-amber-100 text-amber-600' }; + case 'other': + return { label: 'Other', className: 'bg-gray-100 text-gray-600' }; + default: + return { label: 'Meeting', className: 'bg-gray-100 text-gray-400' }; + } + }); + + public readonly meetingStartTime: Signal = computed(() => { + const occurrence = this.occurrence(); + const meeting = this.meeting(); + + // Use occurrence start time if available, otherwise use meeting start time + return occurrence?.start_time || meeting.start_time; + }); + + public readonly formattedTime: Signal = computed(() => { + const startTime = this.meetingStartTime(); + + try { + const meetingDate = new Date(startTime); + + if (isNaN(meetingDate.getTime())) { + return startTime; + } + + const today = new Date(); + const tomorrow = new Date(today); + tomorrow.setDate(tomorrow.getDate() + 1); + + const isToday = meetingDate.toDateString() === today.toDateString(); + const isTomorrow = meetingDate.toDateString() === tomorrow.toDateString(); + + const timeStr = meetingDate.toLocaleTimeString('en-US', { + hour: 'numeric', + minute: '2-digit', + hour12: true, + }); + + if (isToday) { + return `Today, ${timeStr}`; + } else if (isTomorrow) { + return `Tomorrow, ${timeStr}`; + } + const dateStr = meetingDate.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + }); + return `${dateStr} at ${timeStr}`; + } catch { + return startTime; + } + }); + + public readonly isTodayMeeting: Signal = computed(() => { + const startTime = this.meetingStartTime(); + + try { + const meetingDate = new Date(startTime); + + if (isNaN(meetingDate.getTime())) { + return false; + } + + const today = new Date(); + return meetingDate.toDateString() === today.toDateString(); + } catch { + return false; + } + }); + + public readonly isPrivate: Signal = computed(() => { + return this.meeting().visibility === 'private'; + }); + + public readonly hasYoutubeUploads: Signal = computed(() => { + return this.meeting().youtube_upload_enabled === true; + }); + + public readonly hasRecording: Signal = computed(() => { + return this.meeting().recording_enabled === true; + }); + + public readonly hasTranscripts: Signal = computed(() => { + return this.meeting().transcript_enabled === true; + }); + + public readonly hasAiSummary: Signal = computed(() => { + return this.meeting().zoom_config?.ai_companion_enabled === true; + }); + + public readonly meetingTitle: Signal = computed(() => { + const occurrence = this.occurrence(); + const meeting = this.meeting(); + + // Use occurrence title if available, otherwise use meeting title + return occurrence?.title || meeting.title; + }); + + public handleSeeMeeting(): void { + this.onSeeMeeting.emit(this.meeting().uid); + } +} diff --git a/apps/lfx-one/src/app/shared/components/header/header.component.html b/apps/lfx-one/src/app/shared/components/header/header.component.html index b1fe331a..e0c47264 100644 --- a/apps/lfx-one/src/app/shared/components/header/header.component.html +++ b/apps/lfx-one/src/app/shared/components/header/header.component.html @@ -2,20 +2,51 @@
-
+
- +
+ + + + + + @if (userService.authenticated()) { + + } +
- +
+ } + diff --git a/apps/lfx-one/src/app/shared/components/sidebar/sidebar.component.scss b/apps/lfx-one/src/app/shared/components/sidebar/sidebar.component.scss new file mode 100644 index 00000000..df18259a --- /dev/null +++ b/apps/lfx-one/src/app/shared/components/sidebar/sidebar.component.scss @@ -0,0 +1,6 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +:host { + display: block; +} diff --git a/apps/lfx-one/src/app/shared/components/sidebar/sidebar.component.ts b/apps/lfx-one/src/app/shared/components/sidebar/sidebar.component.ts new file mode 100644 index 00000000..3704ebf0 --- /dev/null +++ b/apps/lfx-one/src/app/shared/components/sidebar/sidebar.component.ts @@ -0,0 +1,38 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +import { CommonModule } from '@angular/common'; +import { Component, computed, input } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { BadgeComponent } from '@components/badge/badge.component'; +import { SidebarMenuItem } from '@lfx-one/shared/interfaces'; + +@Component({ + selector: 'lfx-sidebar', + standalone: true, + imports: [CommonModule, RouterModule, BadgeComponent], + templateUrl: './sidebar.component.html', + styleUrl: './sidebar.component.scss', +}) +export class SidebarComponent { + // Input properties + public readonly items = input.required(); + public readonly footerItems = input([]); + public readonly collapsed = input(false); + public readonly styleClass = input(''); + + // Computed items with test IDs + protected readonly itemsWithTestIds = computed(() => + this.items().map((item) => ({ + ...item, + testId: item.testId || `sidebar-item-${item.label.toLowerCase().replace(/\s+/g, '-')}`, + })) + ); + + protected readonly footerItemsWithTestIds = computed(() => + this.footerItems().map((item) => ({ + ...item, + testId: item.testId || `sidebar-item-${item.label.toLowerCase().replace(/\s+/g, '-')}`, + })) + ); +} diff --git a/apps/lfx-one/src/app/shared/services/app.service.ts b/apps/lfx-one/src/app/shared/services/app.service.ts new file mode 100644 index 00000000..9de3e0f1 --- /dev/null +++ b/apps/lfx-one/src/app/shared/services/app.service.ts @@ -0,0 +1,39 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +import { Injectable, signal } from '@angular/core'; + +/** + * Application-wide state management service + * Handles global application state like mobile sidebar visibility + */ +@Injectable({ + providedIn: 'root', +}) +export class AppService { + // Mobile sidebar state + private readonly showMobileSidebarSignal = signal(false); + + public readonly showMobileSidebar = this.showMobileSidebarSignal.asReadonly(); + + /** + * Toggle mobile sidebar visibility + */ + public toggleMobileSidebar(): void { + this.showMobileSidebarSignal.update((value) => !value); + } + + /** + * Close mobile sidebar + */ + public closeMobileSidebar(): void { + this.showMobileSidebarSignal.set(false); + } + + /** + * Open mobile sidebar + */ + public openMobileSidebar(): void { + this.showMobileSidebarSignal.set(true); + } +} diff --git a/apps/lfx-one/src/app/shared/services/persona.service.ts b/apps/lfx-one/src/app/shared/services/persona.service.ts new file mode 100644 index 00000000..9279c9b2 --- /dev/null +++ b/apps/lfx-one/src/app/shared/services/persona.service.ts @@ -0,0 +1,49 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +import { afterNextRender, inject, Injectable, signal, WritableSignal } from '@angular/core'; +import { Router } from '@angular/router'; +import { PersonaType } from '@lfx-one/shared/interfaces'; + +@Injectable({ + providedIn: 'root', +}) +export class PersonaService { + private readonly router = inject(Router); + + private readonly storageKey = 'lfx-persona'; + public readonly currentPersona: WritableSignal; + + public constructor() { + // Initialize with default value + this.currentPersona = signal('core-developer'); + + // Load from localStorage after render (browser only) + afterNextRender(() => { + const stored = localStorage.getItem(this.storageKey) as PersonaType; + if (stored) { + this.currentPersona.set(stored); + } + }); + } + + /** + * Set the current persona and persist to storage + */ + public setPersona(persona: PersonaType): void { + if (persona !== this.currentPersona()) { + this.currentPersona.set(persona); + this.persistPersona(persona); + + if (persona === 'old-ui') { + this.router.navigate(['/old-ui']); + } else { + this.router.navigate(['/']); + } + } + } + + private persistPersona(persona: PersonaType): void { + localStorage.setItem(this.storageKey, persona); + } +} diff --git a/apps/lfx-one/src/server/controllers/profile.controller.ts b/apps/lfx-one/src/server/controllers/profile.controller.ts index ed8ca066..c337ebc0 100644 --- a/apps/lfx-one/src/server/controllers/profile.controller.ts +++ b/apps/lfx-one/src/server/controllers/profile.controller.ts @@ -122,7 +122,7 @@ export class ProfileController { 'postal_code', 'country', 'organization', - 'job_title', + 'title', 't_shirt_size', 'zoneinfo', ]; diff --git a/apps/lfx-one/src/server/services/meeting.service.ts b/apps/lfx-one/src/server/services/meeting.service.ts index 29417c83..33f95270 100644 --- a/apps/lfx-one/src/server/services/meeting.service.ts +++ b/apps/lfx-one/src/server/services/meeting.service.ts @@ -8,8 +8,8 @@ import { MeetingJoinURL, MeetingRegistrant, PastMeetingParticipant, - QueryServiceResponse, QueryServiceCountResponse, + QueryServiceResponse, UpdateMeetingRegistrantRequest, UpdateMeetingRequest, } from '@lfx-one/shared/interfaces'; @@ -22,6 +22,7 @@ import { AccessCheckService } from './access-check.service'; import { CommitteeService } from './committee.service'; import { ETagService } from './etag.service'; import { MicroserviceProxyService } from './microservice-proxy.service'; +import { ProjectService } from './project.service'; /** * Service for handling meeting business logic with microservice proxy @@ -31,11 +32,14 @@ export class MeetingService { private etagService: ETagService; private microserviceProxy: MicroserviceProxyService; private committeeService: CommitteeService; + private projectService: ProjectService; + public constructor() { this.accessCheckService = new AccessCheckService(); this.microserviceProxy = new MicroserviceProxyService(); this.etagService = new ETagService(); this.committeeService = new CommitteeService(); + this.projectService = new ProjectService(); } /** @@ -51,6 +55,9 @@ export class MeetingService { let meetings = resources.map((resource) => resource.data); + // Get project name for each meeting + meetings = await this.getMeetingProjectName(req, meetings); + // Get committee data for each committee associated with the meeting if (meetings.some((m) => m.committees && m.committees.length > 0)) { meetings = await this.getMeetingCommittees(req, meetings); @@ -495,4 +502,15 @@ export class MeetingService { return meetings; } + + private async getMeetingProjectName(req: Request, meetings: Meeting[]): Promise { + const projectUids = [...new Set(meetings.map((m) => m.project_uid))]; + const projects = await Promise.all( + projectUids.map(async (uid) => { + return await this.projectService.getProjectById(req, uid).catch(() => null); + }) + ); + + return meetings.map((m) => ({ ...m, project_name: projects.find((p) => p?.uid === m.project_uid)?.name || '' })); + } } diff --git a/apps/lfx-one/src/server/services/user.service.ts b/apps/lfx-one/src/server/services/user.service.ts index f3a50af4..8ee0d94c 100644 --- a/apps/lfx-one/src/server/services/user.service.ts +++ b/apps/lfx-one/src/server/services/user.service.ts @@ -205,7 +205,7 @@ export class UserService { } // Validate job title if provided (basic length check) - if (metadata?.job_title && metadata.job_title.length > 200) { + if (metadata?.title && metadata.title.length > 200) { throw new Error('Job title is too long'); } diff --git a/packages/shared/package.json b/packages/shared/package.json index 50e640dd..3bdd4c0b 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -53,7 +53,8 @@ }, "peerDependencies": { "@angular/forms": "^19.0.0", - "@fullcalendar/core": "^6.1.19" + "@fullcalendar/core": "^6.1.19", + "chart.js": "^4.5.0" }, "dependencies": { "date-fns": "^4.1.0", diff --git a/packages/shared/src/constants/index.ts b/packages/shared/src/constants/index.ts index 738b6d20..3f6db529 100644 --- a/packages/shared/src/constants/index.ts +++ b/packages/shared/src/constants/index.ts @@ -9,6 +9,7 @@ export * from './countries.constants'; export * from './file-upload.constants'; export * from './font-sizes.constants'; export * from './meeting.constants'; +export * from './persona.constants'; export * from './server.constants'; export * from './states.constants'; export * from './timezones.constants'; diff --git a/packages/shared/src/constants/persona.constants.ts b/packages/shared/src/constants/persona.constants.ts new file mode 100644 index 00000000..00465788 --- /dev/null +++ b/packages/shared/src/constants/persona.constants.ts @@ -0,0 +1,20 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +import { PersonaOption } from '../interfaces'; + +/** + * Persona options available for user selection + */ +export const PERSONA_OPTIONS: PersonaOption[] = [ + { + value: 'core-developer', + label: 'Core Developer', + description: 'New streamlined developer experience', + }, + { + value: 'old-ui', + label: 'Old UI', + description: 'Classic LFX interface', + }, +]; diff --git a/packages/shared/src/interfaces/components.interface.ts b/packages/shared/src/interfaces/components.interface.ts index 789562e9..245777c9 100644 --- a/packages/shared/src/interfaces/components.interface.ts +++ b/packages/shared/src/interfaces/components.interface.ts @@ -1,6 +1,8 @@ // Copyright The Linux Foundation and each contributor to LFX. // SPDX-License-Identifier: MIT +import { ChartData, ChartOptions } from 'chart.js'; + /** * Badge severity level options * @description Available color schemes for badge components @@ -257,3 +259,163 @@ export interface MessageProps { /** Accessibility label */ ariaLabel?: string; } + +/** + * Sidebar menu item configuration + * @description Structure for sidebar navigation menu items + */ +export interface SidebarMenuItem { + /** Display label for menu item */ + label: string; + /** Icon class or name */ + icon: string; + /** Router link path */ + routerLink?: string; + /** External URL */ + url?: string; + /** Badge content for notifications */ + badge?: string | number; + /** Badge severity for styling */ + badgeSeverity?: BadgeSeverityOptions['severity']; + /** Whether item is disabled */ + disabled?: boolean; + /** Command to execute on click */ + command?: () => void; + /** Child menu items for nested navigation */ + items?: SidebarMenuItem[]; + /** Test ID for e2e testing (computed from label if not provided) */ + testId?: string; +} + +/** + * Sidebar component properties + * @description Configuration for LFX sidebar navigation component + */ +export interface SidebarProps { + /** Menu items to display */ + items: SidebarMenuItem[]; + /** Whether sidebar is collapsed */ + collapsed?: boolean; + /** Additional CSS classes */ + styleClass?: string; +} + +/** + * Progress item for dashboard metrics + * @description Structure for progress tracking items + */ +export interface ProgressItem { + /** Metric label */ + label: string; + /** Metric value */ + value: string; + /** Trend direction indicator */ + trend: 'up' | 'down'; +} + +/** + * Progress item with chart data for dashboard metrics + * @description Extended progress item with Chart.js configuration + * @note ChartData and ChartOptions types should be imported from chart.js + */ +export interface ProgressItemWithChart extends ProgressItem { + /** Chart.js data configuration */ + chartData: ChartData<'line'>; + /** Chart.js options configuration */ + chartOptions: ChartOptions<'line'>; + /** Optional subtitle text displayed below the value */ + subtitle?: string; +} + +/** + * Pending action item for task list + * @description Structure for pending action items + */ +export interface PendingActionItem { + /** Action type (e.g., Issue, PR, Review) */ + type: string; + /** Project or repository badge */ + badge: string; + /** Action description text */ + text: string; + /** Icon class for the action type */ + icon: string; + /** Color theme for the action */ + color: 'amber' | 'blue' | 'green' | 'purple'; + /** Button text */ + buttonText: string; +} + +/** + * Meeting item for schedule display + * @description Structure for meeting information + */ +export interface MeetingItem { + /** Meeting title */ + title: string; + /** Meeting time/date */ + time: string; + /** Number of attendees */ + attendees: number; +} + +/** + * Project item for project list + * @description Structure for project information + */ +export interface ProjectItem { + /** Project name */ + name: string; + /** Project logo URL */ + logo?: string; + /** User's role in project */ + role: string; + /** User's affiliations */ + affiliations: string[]; + /** Code activity data for chart */ + codeActivities: number[]; + /** Non-code activity data for chart */ + nonCodeActivities: number[]; + /** Project status */ + status: 'active' | 'archived'; +} + +/** + * Dashboard meeting card feature flags + * @description Enabled features for a meeting displayed on dashboard + */ +export interface DashboardMeetingFeatures { + /** YouTube auto-upload enabled */ + youtubeAutoUploads?: boolean; + /** Recording enabled */ + recordingEnabled: boolean; + /** Transcripts enabled */ + transcriptsEnabled?: boolean; + /** AI summary enabled */ + aiSummary?: boolean; + /** Chat enabled */ + chatEnabled?: boolean; +} + +/** + * Dashboard meeting card properties + * @description Configuration for dashboard meeting card component + */ +export interface DashboardMeetingCardProps { + /** Unique meeting identifier */ + id: string; + /** Meeting title */ + title: string; + /** Meeting date string */ + date: string; + /** Meeting time string */ + time: string; + /** Meeting type category */ + meetingType: string; + /** Whether meeting is private */ + isPrivate: boolean; + /** Enabled meeting features */ + features: DashboardMeetingFeatures; + /** Project name (optional) */ + project?: string; +} diff --git a/packages/shared/src/interfaces/index.ts b/packages/shared/src/interfaces/index.ts index debf4d3e..758a056b 100644 --- a/packages/shared/src/interfaces/index.ts +++ b/packages/shared/src/interfaces/index.ts @@ -57,3 +57,6 @@ export * from './user-statistics.interface'; // Analytics interfaces export * from './analytics.interface'; + +// Persona interfaces +export * from './persona.interface'; diff --git a/packages/shared/src/interfaces/meeting.interface.ts b/packages/shared/src/interfaces/meeting.interface.ts index 604b2644..f6675394 100644 --- a/packages/shared/src/interfaces/meeting.interface.ts +++ b/packages/shared/src/interfaces/meeting.interface.ts @@ -145,6 +145,8 @@ export interface Meeting { attended_count?: number; /** Meeting occurrences */ occurrences: MeetingOccurrence[]; + /** Project name */ + project_name: string; } /** diff --git a/packages/shared/src/interfaces/persona.interface.ts b/packages/shared/src/interfaces/persona.interface.ts new file mode 100644 index 00000000..3659e143 --- /dev/null +++ b/packages/shared/src/interfaces/persona.interface.ts @@ -0,0 +1,23 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +/** + * Available persona types for UI customization + * @description Defines the different user personas that can be selected + */ +export type PersonaType = 'core-developer' | 'old-ui'; + +/** + * Persona option configuration + * @description Structure for persona selection dropdown options + */ +export interface PersonaOption { + /** Unique identifier for the persona */ + value: PersonaType; + /** Display label for the persona */ + label: string; + /** Optional description of the persona */ + description?: string; + /** Optional icon class for visual representation */ + icon?: string; +} diff --git a/packages/shared/src/interfaces/user-profile.interface.ts b/packages/shared/src/interfaces/user-profile.interface.ts index e3270308..7baf50ce 100644 --- a/packages/shared/src/interfaces/user-profile.interface.ts +++ b/packages/shared/src/interfaces/user-profile.interface.ts @@ -1,9 +1,6 @@ // Copyright The Linux Foundation and each contributor to LFX. // SPDX-License-Identifier: MIT -/** - * User profile data from public.users table - */ export interface UserProfile { id: string; email: string; @@ -122,7 +119,7 @@ export interface UserMetadata { name?: string; given_name?: string; family_name?: string; - job_title?: string; + title?: string; organization?: string; country?: string; state_province?: string; diff --git a/yarn.lock b/yarn.lock index 391ae395..f514049f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2879,6 +2879,7 @@ __metadata: peerDependencies: "@angular/forms": ^19.0.0 "@fullcalendar/core": ^6.1.19 + chart.js: ^4.5.0 languageName: unknown linkType: soft