Skip to content

Commit 71d3d78

Browse files
authored
feat: enhance meeting card with expandable text and linkify functionality (#22)
1 parent b6c6f3f commit 71d3d78

File tree

10 files changed

+416
-71
lines changed

10 files changed

+416
-71
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"fullcalendar",
1414
"iconfield",
1515
"inputicon",
16+
"Linkify",
1617
"networkidle",
1718
"nonexistentproject",
1819
"PostgreSQL",

apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-card/meeting-card.component.html

Lines changed: 73 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -57,50 +57,84 @@ <h3 class="text-xl font-display font-semibold text-gray-900 mb-3">
5757
}
5858
</div>
5959

60-
@if (meeting().meeting_type) {
61-
<div class="flex items-center gap-2 mb-4">
62-
<lfx-badge [value]="meeting().meeting_type!" severity="secondary" icon="fa-light fa-calendar-days"></lfx-badge>
63-
</div>
64-
}
65-
66-
@if (meeting().agenda) {
67-
<div class="text-base text-gray-600 pb-3" [innerHTML]="meeting().agenda"></div>
68-
}
69-
70-
<!-- Meeting Settings -->
71-
<div class="flex flex-wrap gap-3 mb-4">
72-
<div class="flex items-center gap-2">
73-
<i [class]="meeting().recording_enabled ? 'fa-light fa-check-circle text-green-500' : 'fa-light fa-times-circle text-gray-400'" class="text-base"></i>
74-
<span class="text-sm text-gray-600">Recording</span>
75-
</div>
76-
<div class="flex flex-wrap gap-3">
77-
<div class="flex items-center gap-2">
78-
<i [class]="meeting().restricted ? 'fa-light fa-check-circle text-green-500' : 'fa-light fa-times-circle text-gray-400'" class="text-base"></i>
79-
<span class="text-sm text-gray-600">Restricted</span>
80-
</div>
81-
</div>
82-
@if (meeting().recording_enabled) {
83-
<div class="flex items-center gap-2">
84-
<i [class]="meeting().youtube_enabled ? 'fa-light fa-check-circle text-green-500' : 'fa-light fa-times-circle text-gray-400'" class="text-base"></i>
85-
<span class="text-sm text-gray-600">YouTube</span>
60+
<div class="flex justify-between">
61+
@if (meeting().meeting_type) {
62+
<div class="flex items-center gap-2 mb-4">
63+
<lfx-badge [value]="meeting().meeting_type!" severity="secondary" icon="fa-light fa-calendar-days"></lfx-badge>
8664
</div>
87-
<div class="flex items-center gap-2">
88-
<i [class]="meeting().zoom_ai_enabled ? 'fa-light fa-check-circle text-green-500' : 'fa-light fa-times-circle text-gray-400'" class="text-base"></i>
89-
<span class="text-sm text-gray-600">Zoom AI</span>
65+
<!-- Meeting Settings -->
66+
<div class="flex gap-2 mb-4">
67+
<i
68+
class="fa-light fa-video text-lg"
69+
[class.text-blue-500]="meeting().recording_enabled"
70+
[class.text-gray-400]="!meeting().recording_enabled"
71+
pTooltip="Recording {{ meeting().recording_enabled ? 'enabled' : 'disabled' }}"
72+
tooltipPosition="top">
73+
</i>
74+
<i
75+
class="fa-light fa-lock text-lg"
76+
[class.text-blue-500]="meeting().restricted"
77+
[class.text-gray-400]="!meeting().restricted"
78+
pTooltip="Meeting {{ meeting().restricted ? 'restricted' : 'not restricted' }}"
79+
tooltipPosition="top">
80+
</i>
81+
<i
82+
class="fa-brands fa-youtube text-lg"
83+
[class.text-blue-500]="meeting().youtube_enabled"
84+
[class.text-gray-400]="!meeting().youtube_enabled"
85+
pTooltip="YouTube {{ meeting().youtube_enabled ? 'enabled' : 'disabled' }}"
86+
tooltipPosition="top">
87+
</i>
88+
<i
89+
class="fa-light fa-microchip-ai text-lg"
90+
[class.text-blue-500]="meeting().zoom_ai_enabled"
91+
[class.text-gray-400]="!meeting().zoom_ai_enabled"
92+
pTooltip="Zoom AI {{ meeting().zoom_ai_enabled ? 'enabled' : 'disabled' }}"
93+
tooltipPosition="top">
94+
</i>
95+
<i
96+
class="fa-light fa-file-lines text-lg"
97+
[class.text-blue-500]="meeting().transcripts_enabled"
98+
[class.text-gray-400]="!meeting().transcripts_enabled"
99+
pTooltip="Transcripts {{ meeting().transcripts_enabled ? 'enabled' : 'disabled' }}"
100+
tooltipPosition="top">
101+
</i>
90102
</div>
91-
<div class="flex items-center gap-2">
92-
<i [class]="meeting().transcripts_enabled ? 'fa-light fa-check-circle text-green-500' : 'fa-light fa-times-circle text-gray-400'" class="text-base"></i>
93-
<span class="text-sm text-gray-600">Transcripts</span>
94-
</div>
95-
@if (meeting().recording_access) {
96-
<div class="flex items-center gap-2">
97-
<i class="fa-light fa-video text-gray-400"></i>
98-
<span class="text-sm text-gray-600">{{ meeting().recording_access }}</span>
99-
</div>
100-
}
101103
}
102104
</div>
103105

106+
@if (meeting().agenda) {
107+
<div class="pb-3">
108+
<lfx-expandable-text [maxHeight]="100">
109+
<div class="flex flex-col gap-4">
110+
<div [innerHTML]="meeting().agenda | linkify"></div>
111+
<!-- Important Links -->
112+
@if (importantLinks().length > 0) {
113+
<div class="mb-4">
114+
<h4 class="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-2">
115+
<i class="fa-light fa-link text-blue-500"></i>
116+
Important Links
117+
</h4>
118+
<div class="flex flex-wrap gap-2">
119+
@for (link of importantLinks(); track link.url) {
120+
<a
121+
[href]="link.url"
122+
target="_blank"
123+
rel="noopener noreferrer"
124+
class="cursor-pointer inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-blue-600 bg-blue-50 hover:bg-blue-100 rounded-md transition-colors"
125+
[title]="link.url">
126+
<i class="fa-light fa-external-link"></i>
127+
{{ link.domain }}
128+
</a>
129+
}
130+
</div>
131+
</div>
132+
}
133+
</div>
134+
</lfx-expandable-text>
135+
</div>
136+
}
137+
104138
<!-- Associated Committees -->
105139
@if (meeting().meeting_committees && meeting().meeting_committees!.length > 0) {
106140
<div class="flex items-start gap-2 mb-4">

apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-card/meeting-card.component.ts

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import { CommonModule } from '@angular/common';
55
import { Component, computed, effect, inject, Injector, input, output, runInInjectionContext, signal, Signal, WritableSignal } from '@angular/core';
66
import { toSignal } from '@angular/core/rxjs-interop';
77
import { RouterLink } from '@angular/router';
8+
import { LinkifyPipe } from '@app/shared/pipes/linkify.pipe';
89
import { AvatarComponent } from '@components/avatar/avatar.component';
910
import { BadgeComponent } from '@components/badge/badge.component';
1011
import { ButtonComponent } from '@components/button/button.component';
12+
import { ExpandableTextComponent } from '@components/expandable-text/expandable-text.component';
1113
import { MenuComponent } from '@components/menu/menu.component';
12-
import { Meeting, MeetingParticipant } from '@lfx-pcc/shared/interfaces';
14+
import { extractUrlsWithDomains, Meeting, MeetingParticipant } from '@lfx-pcc/shared';
1315
import { MeetingTimePipe } from '@pipes/meeting-time.pipe';
1416
import { CommitteeService } from '@services/committee.service';
1517
import { MeetingService } from '@services/meeting.service';
@@ -40,6 +42,8 @@ import { RecurringEditOption, RecurringMeetingEditOptionsComponent } from '../re
4042
TooltipModule,
4143
AnimateOnScrollModule,
4244
ConfirmDialogModule,
45+
ExpandableTextComponent,
46+
LinkifyPipe,
4347
],
4448
providers: [ConfirmationService, MessageService],
4549
templateUrl: './meeting-card.component.html',
@@ -67,6 +71,9 @@ export class MeetingCardComponent {
6771
public readonly meetingDeleted = output<void>();
6872
public readonly project = this.projectService.project;
6973

74+
// Extract important links from agenda
75+
public readonly importantLinks = this.initImportantLinks();
76+
7077
public constructor() {
7178
effect(() => {
7279
this.meeting.set(this.meetingInput());
@@ -139,7 +146,7 @@ export class MeetingCardComponent {
139146
}
140147
private initMeetingParticipantCount(): Signal<number> {
141148
return computed(
142-
() => (this.meeting().individual_participants_count || 0) + (this.meeting().committee_members_count || 0) + (this.additionalParticipantsCount() || 0)
149+
() => (this.meeting()?.individual_participants_count || 0) + (this.meeting()?.committee_members_count || 0) + (this.additionalParticipantsCount() || 0)
143150
);
144151
}
145152

@@ -292,20 +299,18 @@ export class MeetingCardComponent {
292299
icon: 'fa-light fa-edit',
293300
command: () => this.editMeeting(),
294301
});
302+
baseItems.push({
303+
separator: true,
304+
});
295305
}
296306

297307
// Add separator and delete option
298-
baseItems.push(
299-
{
300-
separator: true,
301-
},
302-
{
303-
label: 'Delete',
304-
icon: 'fa-light fa-trash',
305-
styleClass: 'text-red-600',
306-
command: () => this.deleteMeeting(),
307-
}
308-
);
308+
baseItems.push({
309+
label: 'Delete',
310+
icon: 'fa-light fa-trash',
311+
styleClass: 'text-red-600',
312+
command: () => this.deleteMeeting(),
313+
});
309314

310315
return baseItems;
311316
});
@@ -324,4 +329,16 @@ export class MeetingCardComponent {
324329
)
325330
.subscribe();
326331
}
332+
333+
private initImportantLinks(): Signal<{ url: string; domain: string }[]> {
334+
return computed(() => {
335+
const agenda = this.meeting().agenda;
336+
if (!agenda) {
337+
return [];
338+
}
339+
340+
// Use shared utility to extract URLs with domains
341+
return extractUrlsWithDomains(agenda);
342+
});
343+
}
327344
}

apps/lfx-pcc/src/app/modules/project/meetings/components/meeting-form/meeting-form.component.html

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@
1414
<!-- Agenda -->
1515
<div>
1616
<label for="meeting-agenda" class="block text-sm font-medium text-gray-700 mb-1"> Agenda </label>
17-
<lfx-textarea [form]="form()" control="agenda" id="meeting-agenda" placeholder="Enter meeting agenda" [rows]="4" styleClass="w-full"></lfx-textarea>
17+
<lfx-textarea
18+
[form]="form()"
19+
control="agenda"
20+
id="meeting-agenda"
21+
placeholder="Enter meeting agenda"
22+
[rows]="4"
23+
styleClass="w-full max-h-40 overflow-y-auto"></lfx-textarea>
1824
</div>
1925

2026
<!-- Meeting Type -->
@@ -208,24 +214,6 @@ <h3 class="text-lg font-medium text-gray-900">Meeting Settings</h3>
208214

209215
<!-- Recording-dependent features -->
210216
@if (form().get('recording_enabled')?.value === true) {
211-
<div class="flex items-center gap-2">
212-
<lfx-toggle size="small" [form]="form()" control="transcripts_enabled" label="Enable Transcripts" id="transcripts-toggle"></lfx-toggle>
213-
<i
214-
class="fa-light fa-info-circle text-gray-400 cursor-help"
215-
pTooltip="Generate automated transcripts from the meeting recording"
216-
tooltipPosition="right"
217-
tooltipStyleClass="text-xs max-w-xs"></i>
218-
</div>
219-
220-
<div class="flex items-center gap-2">
221-
<lfx-toggle size="small" [form]="form()" control="youtube_enabled" label="Enable YouTube Upload" id="youtube-toggle"></lfx-toggle>
222-
<i
223-
class="fa-light fa-info-circle text-gray-400 cursor-help"
224-
pTooltip="Automatically upload the meeting recording to YouTube after the meeting ends"
225-
tooltipPosition="right"
226-
tooltipStyleClass="text-xs max-w-xs"></i>
227-
</div>
228-
229217
<div>
230218
<div class="flex items-center gap-2 mb-1">
231219
<label for="recording-access" class="block text-sm font-medium text-gray-700">Recording Access</label>
@@ -244,6 +232,23 @@ <h3 class="text-lg font-medium text-gray-900">Meeting Settings</h3>
244232
placeholder="Select recording access level">
245233
</lfx-select>
246234
</div>
235+
<div class="flex items-center gap-2">
236+
<lfx-toggle size="small" [form]="form()" control="transcripts_enabled" label="Enable Transcripts" id="transcripts-toggle"></lfx-toggle>
237+
<i
238+
class="fa-light fa-info-circle text-gray-400 cursor-help"
239+
pTooltip="Generate automated transcripts from the meeting recording"
240+
tooltipPosition="right"
241+
tooltipStyleClass="text-xs max-w-xs"></i>
242+
</div>
243+
244+
<div class="flex items-center gap-2">
245+
<lfx-toggle size="small" [form]="form()" control="youtube_enabled" label="Enable YouTube Upload" id="youtube-toggle"></lfx-toggle>
246+
<i
247+
class="fa-light fa-info-circle text-gray-400 cursor-help"
248+
pTooltip="Automatically upload the meeting recording to YouTube after the meeting ends"
249+
tooltipPosition="right"
250+
tooltipStyleClass="text-xs max-w-xs"></i>
251+
</div>
247252
}
248253
</div>
249254

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
2+
<!-- SPDX-License-Identifier: MIT -->
3+
4+
<div>
5+
<div
6+
#contentElement
7+
class="expandable-content text-base text-gray-600 overflow-hidden transition-all duration-300 ease-in-out break-words relative whitespace-pre-wrap"
8+
[class.expanded]="isExpanded()"
9+
[class.collapsed]="!isExpanded() && needsExpansion()"
10+
[style.height]="getContentHeight()"
11+
data-testid="expandable-content">
12+
<ng-content></ng-content>
13+
</div>
14+
15+
@if (needsExpansion() && isInitialized()) {
16+
<div>
17+
@if (isExpanded()) {
18+
<button
19+
type="button"
20+
class="text-sm text-primary hover:text-primary-600 hover:underline font-medium transition-colors"
21+
(click)="toggleExpanded()"
22+
data-testid="see-less-button">
23+
<i class="fa-light fa-chevron-up mr-1"></i>
24+
See less
25+
</button>
26+
} @else {
27+
<button
28+
type="button"
29+
class="text-sm text-primary hover:text-primary-600 hover:underline font-medium transition-colors"
30+
(click)="toggleExpanded()"
31+
data-testid="read-more-button">
32+
<i class="fa-light fa-chevron-down mr-1"></i>
33+
Read more
34+
</button>
35+
}
36+
</div>
37+
}
38+
</div>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright The Linux Foundation and each contributor to LFX.
2+
// SPDX-License-Identifier: MIT
3+
4+
.expandable-content {
5+
&.collapsed {
6+
&::after {
7+
content: '';
8+
@apply absolute bottom-0 left-0 right-0 h-12 pointer-events-none;
9+
background: linear-gradient(180deg, transparent 0%, rgba(255, 255, 255, 0.5) 40%, rgba(255, 255, 255, 0.9) 70%, rgba(255, 255, 255, 1) 100%);
10+
}
11+
}
12+
13+
&.expanded {
14+
&::after {
15+
@apply hidden;
16+
}
17+
}
18+
}

0 commit comments

Comments
 (0)