Skip to content

Commit c245f33

Browse files
committed
feat(dashboard): implement dashboard meeting card component
- Create reusable DashboardMeetingCardComponent for displaying meeting information - Update MyMeetingsComponent to use new card component with Today/Upcoming sections - Add conditional section rendering (hide sections when empty) - Match React component styling with white card background, gray icons, rounded-xl borders - Implement smart date filtering for today's meetings vs upcoming meetings - Add meeting feature icons (YouTube, Recording, Transcripts, AI, Public/Private) - Add DashboardMeetingCardProps and DashboardMeetingFeatures interfaces to shared package - Update RecentProgressComponent styling to match React design - Remove PrimeNG Card dependency in favor of semantic HTML with Tailwind LFXV2-644 Signed-off-by: Asitha de Silva <asithade@gmail.com>
1 parent 52f816d commit c245f33

File tree

7 files changed

+511
-181
lines changed

7 files changed

+511
-181
lines changed
Lines changed: 53 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,64 @@
11
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
22
<!-- SPDX-License-Identifier: MIT -->
33

4-
<lfx-card data-testid="dashboard-my-meetings-card" styleClass="hover:shadow-lg transition-shadow h-full flex flex-col">
5-
<ng-template #header>
6-
<div class="flex items-center gap-3 p-6 border-b border-gray-100">
7-
<div class="w-12 h-12 rounded-lg bg-gray-50 flex items-center justify-center">
8-
<i class="fa-light fa-calendar-days text-2xl text-green-500"></i>
9-
</div>
10-
<h2 class="text-base font-display font-medium text-gray-900">My Meetings</h2>
11-
</div>
12-
</ng-template>
4+
<section class="flex flex-col flex-1" data-testid="dashboard-my-meetings-section">
5+
<!-- Header -->
6+
<div class="flex items-center justify-between mb-4">
7+
<h2 class="font-display font-semibold text-gray-900">My Meetings</h2>
8+
<lfx-button
9+
label="View All"
10+
icon="fa-light fa-chevron-right"
11+
iconPos="right"
12+
(onClick)="handleViewAll()"
13+
styleClass="!text-sm !font-normal"
14+
[text]="true"
15+
size="small"
16+
data-testid="dashboard-my-meetings-view-all" />
17+
</div>
1318

14-
<div class="p-6 flex-1 flex flex-col">
15-
<div class="space-y-4 flex-1" data-testid="dashboard-my-meetings-list">
16-
@for (item of meetings(); track item.time) {
17-
<div
18-
class="p-4 bg-white border border-gray-200 rounded-lg hover:border-gray-300 transition-colors"
19-
[attr.data-testid]="'dashboard-my-meetings-item-' + item.title">
20-
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
21-
<div class="flex items-center gap-3">
22-
<div class="w-10 h-10 rounded-lg bg-green-100 flex items-center justify-center flex-shrink-0">
23-
<i class="fa-light fa-calendar text-green-600"></i>
24-
</div>
25-
<div>
26-
<div class="font-semibold text-gray-900 text-sm">{{ item.title }}</div>
27-
<div class="text-xs text-gray-500">{{ item.time }}</div>
28-
</div>
19+
<!-- Scrollable Content -->
20+
<div class="flex flex-col flex-1">
21+
<div class="flex flex-col gap-6" data-testid="dashboard-my-meetings-list">
22+
@if (todayMeetings().length > 0 || upcomingMeetings().length > 0) {
23+
<!-- TODAY Section - only show if there are meetings today -->
24+
@if (todayMeetings().length > 0) {
25+
<div>
26+
<h4 class="text-xs font-medium text-gray-500 mb-3 uppercase tracking-wide">Today</h4>
27+
<div class="flex flex-col gap-3">
28+
@for (item of todayMeetings(); track item.meeting.uid) {
29+
<lfx-dashboard-meeting-card
30+
[meeting]="item.meeting"
31+
[occurrence]="item.occurrence"
32+
[projectName]="'Kubernetes'"
33+
(onSeeMeeting)="handleSeeMeeting($event)"
34+
[attr.data-testid]="'dashboard-my-meetings-today-item-' + item.meeting.uid" />
35+
}
2936
</div>
30-
<div class="flex items-center gap-2 justify-between sm:justify-start w-full sm:w-auto">
31-
<span class="inline-flex items-center gap-1 px-2 py-1 rounded-md bg-gray-100 text-xs text-gray-600" data-testid="dashboard-my-meetings-attendees">
32-
<i class="fa-light fa-users text-xs"></i>
33-
{{ item.attendees }}
34-
</span>
35-
<button
36-
type="button"
37-
(click)="handleJoinMeeting(item)"
38-
class="px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white text-sm font-medium rounded-md transition-colors min-h-[44px] sm:min-h-0"
39-
data-testid="dashboard-my-meetings-join-button"
40-
aria-label="Join meeting">
41-
Join
42-
</button>
37+
</div>
38+
}
39+
40+
<!-- UPCOMING Section - only show if there are upcoming meetings -->
41+
@if (upcomingMeetings().length > 0) {
42+
<div>
43+
<h4 class="text-xs font-medium text-gray-500 mb-3 uppercase tracking-wide">Upcoming</h4>
44+
<div class="flex flex-col gap-3">
45+
@for (item of upcomingMeetings(); track item.meeting.uid) {
46+
<lfx-dashboard-meeting-card
47+
[meeting]="item.meeting"
48+
[occurrence]="item.occurrence"
49+
[projectName]="'Kubernetes'"
50+
(onSeeMeeting)="handleSeeMeeting($event)"
51+
[attr.data-testid]="'dashboard-my-meetings-upcoming-item-' + item.meeting.uid" />
52+
}
4353
</div>
4454
</div>
55+
}
56+
} @else {
57+
<!-- Global empty state - only shows when no meetings at all -->
58+
<div class="text-xs text-gray-500 py-8 text-center border-2 border-dashed border-gray-300 rounded-lg" data-testid="dashboard-my-meetings-empty">
59+
No meetings scheduled
4560
</div>
4661
}
4762
</div>
4863
</div>
49-
</lfx-card>
64+
</section>

apps/lfx-one/src/app/modules/pages/dashboard/components/my-meetings/my-meetings.component.ts

Lines changed: 82 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2,57 +2,68 @@
22
// SPDX-License-Identifier: MIT
33

44
import { CommonModule } from '@angular/common';
5-
import { Component, computed, inject, output } from '@angular/core';
5+
import { Component, computed, inject } from '@angular/core';
66
import { toSignal } from '@angular/core/rxjs-interop';
7+
import { Router } from '@angular/router';
78
import { MeetingService } from '@app/shared/services/meeting.service';
8-
import { CardComponent } from '@components/card/card.component';
9+
import { ButtonComponent } from '@components/button/button.component';
10+
import { DashboardMeetingCardComponent } from '@components/dashboard-meeting-card/dashboard-meeting-card.component';
911

10-
import type { Meeting, MeetingItem, MeetingOccurrence } from '@lfx-one/shared/interfaces';
12+
import type { Meeting, MeetingOccurrence } from '@lfx-one/shared/interfaces';
13+
14+
interface MeetingWithOccurrence {
15+
meeting: Meeting;
16+
occurrence: MeetingOccurrence;
17+
sortTime: number;
18+
}
1119

1220
@Component({
1321
selector: 'lfx-my-meetings',
1422
standalone: true,
15-
imports: [CommonModule, CardComponent],
23+
imports: [CommonModule, DashboardMeetingCardComponent, ButtonComponent],
1624
templateUrl: './my-meetings.component.html',
1725
styleUrl: './my-meetings.component.scss',
1826
})
1927
export class MyMeetingsComponent {
2028
private readonly meetingService = inject(MeetingService);
29+
private readonly router = inject(Router);
2130
private readonly allMeetings = toSignal(this.meetingService.getMeetings(), { initialValue: [] });
2231

23-
public readonly joinMeeting = output<MeetingItem>();
24-
25-
protected readonly meetings = computed<MeetingItem[]>(() => {
32+
protected readonly todayMeetings = computed<MeetingWithOccurrence[]>(() => {
2633
const now = new Date();
34+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
35+
const todayEnd = new Date(today.getTime() + 24 * 60 * 60 * 1000);
2736
const currentTime = now.getTime();
2837
const buffer = 40 * 60 * 1000; // 40 minutes in milliseconds
2938

30-
const upcomingMeetings: Array<{ meeting: Meeting; occurrence: MeetingOccurrence; sortTime: number }> = [];
39+
const meetings: MeetingWithOccurrence[] = [];
3140

3241
for (const meeting of this.allMeetings()) {
3342
// Process occurrences if they exist
3443
if (meeting.occurrences && meeting.occurrences.length > 0) {
3544
for (const occurrence of meeting.occurrences) {
36-
const startTime = new Date(occurrence.start_time).getTime();
37-
const endTime = startTime + occurrence.duration * 60 * 1000 + buffer;
45+
const startTime = new Date(occurrence.start_time);
46+
const startTimeMs = startTime.getTime();
47+
const endTime = startTimeMs + occurrence.duration * 60 * 1000 + buffer;
3848

39-
// Only include if meeting hasn't ended yet (including buffer)
40-
if (endTime >= currentTime) {
41-
upcomingMeetings.push({
49+
// Include if meeting is today and hasn't ended yet (including buffer)
50+
if (startTime >= today && startTime < todayEnd && endTime >= currentTime) {
51+
meetings.push({
4252
meeting,
4353
occurrence,
44-
sortTime: startTime,
54+
sortTime: startTimeMs,
4555
});
4656
}
4757
}
4858
} else {
4959
// Handle meetings without occurrences (single meetings)
50-
const startTime = new Date(meeting.start_time).getTime();
51-
const endTime = startTime + meeting.duration * 60 * 1000 + buffer;
60+
const startTime = new Date(meeting.start_time);
61+
const startTimeMs = startTime.getTime();
62+
const endTime = startTimeMs + meeting.duration * 60 * 1000 + buffer;
5263

53-
// Only include if meeting hasn't ended yet (including buffer)
54-
if (endTime >= currentTime) {
55-
upcomingMeetings.push({
64+
// Include if meeting is today and hasn't ended yet (including buffer)
65+
if (startTime >= today && startTime < todayEnd && endTime >= currentTime) {
66+
meetings.push({
5667
meeting,
5768
occurrence: {
5869
occurrence_id: '',
@@ -61,52 +72,70 @@ export class MyMeetingsComponent {
6172
start_time: meeting.start_time,
6273
duration: meeting.duration,
6374
},
64-
sortTime: startTime,
75+
sortTime: startTimeMs,
6576
});
6677
}
6778
}
6879
}
6980

70-
// Sort by earliest time first and limit to 5
71-
return upcomingMeetings
72-
.sort((a, b) => a.sortTime - b.sortTime)
73-
.slice(0, 5)
74-
.map((item) => ({
75-
title: item.occurrence.title,
76-
time: this.formatMeetingTime(item.occurrence.start_time),
77-
attendees: item.meeting.individual_registrants_count + item.meeting.committee_members_count,
78-
}));
81+
// Sort by earliest time first
82+
return meetings.sort((a, b) => a.sortTime - b.sortTime);
7983
});
8084

81-
public handleJoinMeeting(meeting: MeetingItem): void {
82-
this.joinMeeting.emit(meeting);
83-
}
84-
85-
private formatMeetingTime(startTime: string): string {
86-
const meetingDate = new Date(startTime);
85+
protected readonly upcomingMeetings = computed<MeetingWithOccurrence[]>(() => {
8786
const now = new Date();
8887
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
89-
const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000);
90-
const meetingDateOnly = new Date(meetingDate.getFullYear(), meetingDate.getMonth(), meetingDate.getDate());
88+
const todayEnd = new Date(today.getTime() + 24 * 60 * 60 * 1000);
9189

92-
const timeFormatter = new Intl.DateTimeFormat('en-US', {
93-
hour: 'numeric',
94-
minute: '2-digit',
95-
hour12: true,
96-
});
90+
const meetings: MeetingWithOccurrence[] = [];
91+
92+
for (const meeting of this.allMeetings()) {
93+
// Process occurrences if they exist
94+
if (meeting.occurrences && meeting.occurrences.length > 0) {
95+
for (const occurrence of meeting.occurrences) {
96+
const startTime = new Date(occurrence.start_time);
97+
const startTimeMs = startTime.getTime();
9798

98-
const formattedTime = timeFormatter.format(meetingDate);
99+
// Include if meeting is after today
100+
if (startTime >= todayEnd) {
101+
meetings.push({
102+
meeting,
103+
occurrence,
104+
sortTime: startTimeMs,
105+
});
106+
}
107+
}
108+
} else {
109+
// Handle meetings without occurrences (single meetings)
110+
const startTime = new Date(meeting.start_time);
111+
const startTimeMs = startTime.getTime();
99112

100-
if (meetingDateOnly.getTime() === today.getTime()) {
101-
return `Today, ${formattedTime}`;
102-
} else if (meetingDateOnly.getTime() === tomorrow.getTime()) {
103-
return `Tomorrow, ${formattedTime}`;
113+
// Include if meeting is after today
114+
if (startTime >= todayEnd) {
115+
meetings.push({
116+
meeting,
117+
occurrence: {
118+
occurrence_id: '',
119+
title: meeting.title,
120+
description: meeting.description,
121+
start_time: meeting.start_time,
122+
duration: meeting.duration,
123+
},
124+
sortTime: startTimeMs,
125+
});
126+
}
127+
}
104128
}
105-
const dateFormatter = new Intl.DateTimeFormat('en-US', {
106-
weekday: 'long',
107-
month: 'short',
108-
day: 'numeric',
109-
});
110-
return `${dateFormatter.format(meetingDate)}, ${formattedTime}`;
129+
130+
// Sort by earliest time first and limit to 5
131+
return meetings.sort((a, b) => a.sortTime - b.sortTime).slice(0, 5);
132+
});
133+
134+
public handleSeeMeeting(meetingId: string): void {
135+
this.router.navigate(['/meetings', meetingId]);
136+
}
137+
138+
public handleViewAll(): void {
139+
this.router.navigate(['/meetings']);
111140
}
112141
}
Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,63 @@
11
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
22
<!-- SPDX-License-Identifier: MIT -->
33

4-
<lfx-card data-testid="dashboard-recent-progress-card" styleClass="hover:shadow-lg transition-shadow">
5-
<ng-template #header>
6-
<div class="flex items-center justify-between p-6 border-b border-gray-100">
7-
<div class="flex items-center gap-3">
8-
<div class="w-12 h-12 rounded-lg bg-gray-50 flex items-center justify-center">
9-
<i class="fa-light fa-chart-line text-2xl text-blue-500"></i>
10-
</div>
11-
<h2 class="text-base font-display font-medium text-gray-900">Recent Progress</h2>
12-
</div>
13-
<div class="flex gap-2">
4+
<section data-testid="dashboard-recent-progress-section">
5+
<div class="flex items-center justify-between mb-4">
6+
<div>
7+
<h2 class="font-['Roboto_Slab'] font-semibold text-[16px]">Recent Progress</h2>
8+
</div>
9+
10+
<div class="flex items-center gap-3">
11+
<!-- View Profile Button -->
12+
<button
13+
type="button"
14+
class="text-sm px-3 py-1 rounded hover:bg-gray-100 text-gray-700 hover:text-gray-900 flex items-center gap-1 transition-colors"
15+
data-testid="dashboard-recent-progress-view-profile"
16+
aria-label="View Profile">
17+
View Profile
18+
<i class="fa-light fa-chevron-right text-xs"></i>
19+
</button>
20+
21+
<!-- Carousel Controls -->
22+
<div class="flex items-center gap-2">
1423
<button
1524
type="button"
1625
(click)="scrollLeft()"
17-
class="w-8 h-8 flex items-center justify-center rounded-full border border-gray-300 text-gray-600 hover:bg-gray-50 transition-colors"
26+
class="h-8 w-8 p-0 flex items-center justify-center rounded border border-gray-300 bg-white text-gray-600 hover:bg-gray-100 hover:border-gray-400 transition-colors"
1827
data-testid="dashboard-recent-progress-scroll-left"
1928
aria-label="Scroll left">
2029
<i class="fa-light fa-chevron-left"></i>
2130
</button>
2231
<button
2332
type="button"
2433
(click)="scrollRight()"
25-
class="w-8 h-8 flex items-center justify-center rounded-full border border-gray-300 text-gray-600 hover:bg-gray-50 transition-colors"
34+
class="h-8 w-8 p-0 flex items-center justify-center rounded border border-gray-300 bg-white text-gray-600 hover:bg-gray-100 hover:border-gray-400 transition-colors"
2635
data-testid="dashboard-recent-progress-scroll-right"
2736
aria-label="Scroll right">
2837
<i class="fa-light fa-chevron-right"></i>
2938
</button>
3039
</div>
3140
</div>
32-
</ng-template>
41+
</div>
3342

34-
<div class="p-6">
43+
<div class="overflow-hidden">
3544
<div #progressScroll class="flex gap-4 overflow-x-auto pb-2 hide-scrollbar scroll-smooth" data-testid="dashboard-recent-progress-items">
3645
@for (item of progressItems; track item.label) {
37-
<div
38-
class="bg-white border border-gray-200 rounded-lg p-4 min-w-[280px] flex-shrink-0"
39-
[attr.data-testid]="'dashboard-recent-progress-item-' + item.label">
40-
<div class="flex items-center justify-between mb-3">
41-
<div>
42-
<div class="text-sm text-gray-600 mb-1">{{ item.label }}</div>
43-
<div class="text-2xl font-display font-semibold text-gray-900">{{ item.value }}</div>
46+
<div class="p-4 bg-white rounded-lg border border-slate-200 flex-shrink-0 w-80" [attr.data-testid]="'dashboard-recent-progress-item-' + item.label">
47+
<div class="space-y-3">
48+
<h5 class="text-sm font-medium w-full">{{ item.label }}</h5>
49+
<div class="w-full h-8">
50+
<lfx-chart type="line" [data]="item.chartData" [options]="item.chartOptions" height="100%"></lfx-chart>
51+
</div>
52+
<div class="space-y-1">
53+
<div class="text-xl font-medium">{{ item.value }}</div>
54+
@if (item.subtitle) {
55+
<div class="text-xs text-gray-500">{{ item.subtitle }}</div>
56+
}
4457
</div>
45-
@if (item.trend === 'up') {
46-
<i class="fa-light fa-arrow-trend-up text-green-500 text-lg" data-testid="dashboard-recent-progress-trend-up"></i>
47-
} @else if (item.trend === 'down') {
48-
<i class="fa-light fa-arrow-trend-down text-red-500 text-lg" data-testid="dashboard-recent-progress-trend-down"></i>
49-
}
50-
</div>
51-
<div class="h-20">
52-
<lfx-chart type="line" [data]="item.chartData" [options]="item.chartOptions" height="100%"></lfx-chart>
5358
</div>
5459
</div>
5560
}
5661
</div>
5762
</div>
58-
</lfx-card>
63+
</section>

0 commit comments

Comments
 (0)