Skip to content

Commit 6028208

Browse files
authored
feat(meetings): update join component ui to match design (#143)
* feat(meetings): update join component ui to match react design - Add RSVP container for future meetings with lfx-button components - Implement compact date/time format (Wed, Aug 26 • 11:30 PM - 12:30 AM) - Restructure layout to two-column design matching React implementation - Fix ESLint no-case-declarations violations in MeetingTimePipe - Replace title attribute with pTooltip in organization involvement component LFXV2-710 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <asithade@gmail.com> * feat(meetings): add rsvp button group and scope modal components - Add new RsvpButtonGroupComponent for streamlined RSVP selection - Add new RsvpScopeModalComponent for occurrence-specific RSVP handling - Integrate RSVP components into meeting join and card views - Refactor backend RSVP endpoints for occurrence-specific support - Update meeting service to use M2M tokens for RSVP retrieval - Add recurring badge to meeting join component - Improve RSVP API route organization and naming consistency LFXV2-710 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <asithade@gmail.com> --------- Signed-off-by: Asitha de Silva <asithade@gmail.com>
1 parent 5147707 commit 6028208

File tree

21 files changed

+888
-440
lines changed

21 files changed

+888
-440
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"PostgreSQL",
2626
"PostgREST",
2727
"primeng",
28+
"rsvps",
2829
"sparkline",
2930
"styleclass",
3031
"supabase",

apps/lfx-one/src/app/modules/dashboards/components/organization-involvement/organization-involvement.component.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ <h2 class="font-['Roboto_Slab'] font-semibold text-[16px]">Organization Involvem
3030
<h3 class="text-sm font-medium">{{ metric.title }}</h3>
3131
<span
3232
[class]="metric.isConnected ? 'text-green-500' : 'text-gray-400'"
33-
[title]="metric.isConnected ? 'Connected to live data' : 'Using placeholder data'"
33+
[pTooltip]="metric.isConnected ? 'Connected to live data' : 'Using placeholder data'"
3434
class="text-xs cursor-help">
3535
3636
</span>
@@ -66,7 +66,7 @@ <h3 class="text-sm font-medium">{{ metric.title }}</h3>
6666
<h3 class="text-sm font-medium">{{ metric.title }}</h3>
6767
<span
6868
[class]="metric.isConnected ? 'text-green-500' : 'text-gray-400'"
69-
[title]="metric.isConnected ? 'Connected to live data' : 'Using placeholder data'"
69+
[pTooltip]="metric.isConnected ? 'Connected to live data' : 'Using placeholder data'"
7070
class="text-xs cursor-help">
7171
7272
</span>
@@ -105,10 +105,10 @@ <h3 class="text-sm font-medium mb-3">Our Contributions</h3>
105105
<div class="flex items-center justify-between w-full py-2.5 px-3 rounded-lg" [attr.data-testid]="'contributions-metric-' + metric.title">
106106
<span class="text-sm text-gray-500 flex items-center gap-1">
107107
{{ metric.title }}
108-
<i class="fa-light fa-circle-question w-3 h-3 cursor-help" [title]="metric.tooltip"></i>
108+
<i class="fa-light fa-circle-question w-3 h-3 cursor-help" [pTooltip]="metric.tooltip"></i>
109109
<span
110110
[class]="metric.isConnected ? 'text-green-500' : 'text-gray-400'"
111-
[title]="metric.isConnected ? 'Connected to live data' : 'Using placeholder data'"
111+
[pTooltip]="metric.isConnected ? 'Connected to live data' : 'Using placeholder data'"
112112
class="text-xs cursor-help">
113113
114114
</span>
@@ -127,10 +127,10 @@ <h3 class="text-sm font-medium mb-3">Our Impact</h3>
127127
<div class="flex items-center justify-between w-full py-2.5 px-3 rounded-lg" [attr.data-testid]="'impact-metric-' + metric.title">
128128
<span class="text-sm text-gray-500 flex items-center gap-1">
129129
{{ metric.title }}
130-
<i class="fa-light fa-circle-question w-3 h-3 cursor-help" [title]="metric.tooltip"></i>
130+
<i class="fa-light fa-circle-question w-3 h-3 cursor-help" [pTooltip]="metric.tooltip"></i>
131131
<span
132132
[class]="metric.isConnected ? 'text-green-500' : 'text-gray-400'"
133-
[title]="metric.isConnected ? 'Connected to live data' : 'Using placeholder data'"
133+
[pTooltip]="metric.isConnected ? 'Connected to live data' : 'Using placeholder data'"
134134
class="text-xs cursor-help">
135135
136136
</span>

apps/lfx-one/src/app/modules/dashboards/components/organization-involvement/organization-involvement.component.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ import { ChartComponent } from '@components/chart/chart.component';
1010
import { CONTRIBUTIONS_METRICS, IMPACT_METRICS, PRIMARY_INVOLVEMENT_METRICS } from '@lfx-one/shared/constants';
1111
import { ContributionMetric, ImpactMetric, OrganizationInvolvementMetricWithChart, PrimaryInvolvementMetric } from '@lfx-one/shared/interfaces';
1212
import { hexToRgba } from '@lfx-one/shared/utils';
13+
import { TooltipModule } from 'primeng/tooltip';
1314
import { finalize, map, switchMap } from 'rxjs';
1415

1516
@Component({
1617
selector: 'lfx-organization-involvement',
1718
standalone: true,
18-
imports: [CommonModule, ChartComponent],
19+
imports: [CommonModule, ChartComponent, TooltipModule],
1920
providers: [CurrencyPipe],
2021
templateUrl: './organization-involvement.component.html',
2122
styleUrl: './organization-involvement.component.scss',

apps/lfx-one/src/app/modules/meetings/meeting-join/meeting-join.component.html

Lines changed: 286 additions & 303 deletions
Large diffs are not rendered by default.

apps/lfx-one/src/app/modules/meetings/meeting-join/meeting-join.component.ts

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright The Linux Foundation and each contributor to LFX.
22
// SPDX-License-Identifier: MIT
33

4+
import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard';
45
import { CommonModule } from '@angular/common';
56
import { HttpParams } from '@angular/common/http';
67
import { Component, computed, inject, signal, Signal, WritableSignal } from '@angular/core';
@@ -13,7 +14,7 @@ import { ButtonComponent } from '@components/button/button.component';
1314
import { CardComponent } from '@components/card/card.component';
1415
import { ExpandableTextComponent } from '@components/expandable-text/expandable-text.component';
1516
import { InputTextComponent } from '@components/input-text/input-text.component';
16-
import { MessageComponent } from '@components/message/message.component';
17+
import { RsvpButtonGroupComponent } from '@components/rsvp-button-group/rsvp-button-group.component';
1718
import { environment } from '@environments/environment';
1819
import {
1920
canJoinMeeting,
@@ -25,7 +26,7 @@ import {
2526
Project,
2627
User,
2728
} from '@lfx-one/shared';
28-
import { FileSizePipe } from '@pipes/file-size.pipe';
29+
import { FileTypeIconPipe } from '@pipes/file-type-icon.pipe';
2930
import { MeetingTimePipe } from '@pipes/meeting-time.pipe';
3031
import { MeetingService } from '@services/meeting.service';
3132
import { UserService } from '@services/user.service';
@@ -38,18 +39,19 @@ import { catchError, combineLatest, debounceTime, filter, map, Observable, of, s
3839
selector: 'lfx-meeting-join',
3940
standalone: true,
4041
imports: [
42+
ClipboardModule,
4143
CommonModule,
4244
ReactiveFormsModule,
4345
ButtonComponent,
4446
CardComponent,
4547
InputTextComponent,
46-
MessageComponent,
48+
RsvpButtonGroupComponent,
4749
ToastModule,
4850
TooltipModule,
4951
MeetingTimePipe,
5052
RecurrenceSummaryPipe,
5153
LinkifyPipe,
52-
FileSizePipe,
54+
FileTypeIconPipe,
5355
ExpandableTextComponent,
5456
],
5557
providers: [],
@@ -62,6 +64,7 @@ export class MeetingJoinComponent {
6264
private readonly router = inject(Router);
6365
private readonly meetingService = inject(MeetingService);
6466
private readonly userService = inject(UserService);
67+
private readonly clipboard = inject(Clipboard);
6568

6669
// Class variables with types
6770
public authenticated: WritableSignal<boolean>;
@@ -75,13 +78,13 @@ export class MeetingJoinComponent {
7578
public returnTo: Signal<string | undefined>;
7679
public password: WritableSignal<string | null> = signal<string | null>(null);
7780
public canJoinMeeting: Signal<boolean>;
78-
public joinUrlWithParams: Signal<string | undefined>;
7981
public fetchedJoinUrl: Signal<string | undefined>;
8082
public isLoadingJoinUrl: WritableSignal<boolean> = signal<boolean>(false);
8183
public joinUrlError: WritableSignal<string | null> = signal<string | null>(null);
8284
public attachments: Signal<MeetingAttachment[]>;
8385
public messageSeverity: Signal<'success' | 'info' | 'warn'>;
8486
public messageIcon: Signal<string>;
87+
public alertMessage: Signal<string>;
8588
private hasAutoJoined: WritableSignal<boolean> = signal<boolean>(false);
8689

8790
// Form value signals for reactivity
@@ -98,14 +101,25 @@ export class MeetingJoinComponent {
98101
this.importantLinks = this.initializeImportantLinks();
99102
this.returnTo = this.initializeReturnTo();
100103
this.canJoinMeeting = this.initializeCanJoinMeeting();
101-
this.joinUrlWithParams = this.initializeJoinUrlWithParams();
102104
this.fetchedJoinUrl = this.initializeFetchedJoinUrl();
103105
this.attachments = this.initializeAttachments();
104106
this.messageSeverity = this.initializeMessageSeverity();
105107
this.messageIcon = this.initializeMessageIcon();
108+
this.alertMessage = this.initializeAlertMessage();
106109
this.initializeAutoJoin();
107110
}
108111

112+
public handleCopyLink(): void {
113+
const meetingUrl: URL = new URL(environment.urls.home + '/meetings/' + this.meeting().uid);
114+
meetingUrl.searchParams.set('password', this.password() || '');
115+
this.clipboard.copy(meetingUrl.toString());
116+
this.messageService.add({
117+
severity: 'success',
118+
summary: 'Meeting Link Copied',
119+
detail: 'The meeting link has been copied to your clipboard',
120+
});
121+
}
122+
109123
private initializeAutoJoin(): void {
110124
// Use toObservable to create an Observable from the signals, then subscribe once
111125
// This executes only when all conditions are met
@@ -271,21 +285,6 @@ export class MeetingJoinComponent {
271285
});
272286
}
273287

274-
private initializeJoinUrlWithParams(): Signal<string | undefined> {
275-
return computed(() => {
276-
const meeting = this.meeting();
277-
const joinUrl = meeting?.join_url;
278-
279-
if (!joinUrl) {
280-
return undefined;
281-
}
282-
283-
// Access form values to trigger reactivity
284-
const formValues = this.formValues();
285-
return this.buildJoinUrlWithParams(joinUrl, formValues);
286-
});
287-
}
288-
289288
private buildJoinUrlWithParams(joinUrl: string, formValues?: { name: string; email: string; organization: string }): string {
290289
if (!joinUrl) {
291290
return joinUrl;
@@ -425,4 +424,17 @@ export class MeetingJoinComponent {
425424
{ initialValue: [] }
426425
);
427426
}
427+
428+
private initializeAlertMessage(): Signal<string> {
429+
return computed(() => {
430+
const canJoin = this.canJoinMeeting();
431+
const meeting = this.meeting();
432+
const earlyJoinMinutes = meeting?.early_join_time_minutes || 10;
433+
434+
if (canJoin) {
435+
return 'The meeting is in progress.';
436+
}
437+
return `You may only join the meeting up to ${earlyJoinMinutes} minutes before the start time.`;
438+
});
439+
}
428440
}

apps/lfx-one/src/app/shared/components/button/button.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export class ButtonComponent {
5353
// Navigation
5454
public readonly routerLink = input<string | string[] | undefined>(undefined);
5555
public readonly href = input<string | undefined>(undefined);
56-
public readonly target = input<string | undefined>(undefined);
56+
public readonly target = input<string | undefined>('_self');
5757
public readonly rel = input<string | undefined>(undefined);
5858
public readonly queryParams = input<Record<string, string>>({});
5959

apps/lfx-one/src/app/shared/components/meeting-card/meeting-card.component.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,13 @@ <h3 class="text-base font-medium text-gray-900 leading-tight tracking-tight" dat
243243
</div>
244244
}
245245

246+
<!-- RSVP Section - Show for authenticated users on upcoming meetings -->
247+
@if (!pastMeeting() && authenticated() && !meeting().organizer) {
248+
<div class="mt-3.5">
249+
<lfx-rsvp-button-group [meeting]="meeting()" [occurrenceId]="occurrence()?.occurrence_id" [showHeader]="false"> </lfx-rsvp-button-group>
250+
</div>
251+
}
252+
246253
<!-- Past Meeting Buttons -->
247254
@if (pastMeeting()) {
248255
<div class="flex gap-2 mt-3.5">

apps/lfx-one/src/app/shared/components/meeting-card/meeting-card.component.ts

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
MeetingDeleteTypeSelectionComponent,
2020
} from '@components/meeting-delete-type-selection/meeting-delete-type-selection.component';
2121
import { MenuComponent } from '@components/menu/menu.component';
22+
import { RsvpButtonGroupComponent } from '@components/rsvp-button-group/rsvp-button-group.component';
2223
import { environment } from '@environments/environment';
2324
import {
2425
buildJoinUrlWithParams,
@@ -70,6 +71,7 @@ import { BehaviorSubject, catchError, combineLatest, filter, finalize, map, of,
7071
FileTypeIconPipe,
7172
FileSizePipe,
7273
ClipboardModule,
74+
RsvpButtonGroupComponent,
7375
],
7476
providers: [ConfirmationService],
7577
templateUrl: './meeting-card.component.html',
@@ -88,8 +90,7 @@ export class MeetingCardComponent implements OnInit {
8890
public readonly pastMeeting = input<boolean>(false);
8991
public readonly loading = input<boolean>(false);
9092
public readonly showBorder = input<boolean>(false);
91-
public readonly meetingRegistrantCount: Signal<number> = this.initMeetingRegistrantCount();
92-
public readonly registrantResponseBreakdown: Signal<string> = this.initRegistrantResponseBreakdown();
93+
9394
public showRegistrants: WritableSignal<boolean> = signal(false);
9495
public meeting: WritableSignal<Meeting | PastMeeting> = signal({} as Meeting | PastMeeting);
9596
public occurrence: WritableSignal<MeetingOccurrence | null> = signal(null);
@@ -99,26 +100,22 @@ export class MeetingCardComponent implements OnInit {
99100
public pastMeetingParticipants = this.initPastMeetingParticipantsList();
100101
public registrantsLabel: Signal<string> = this.initRegistrantsLabel();
101102
public recording: WritableSignal<PastMeetingRecording | null> = signal(null);
102-
public recordingShareUrl: Signal<string | null> = computed(() => {
103-
const recording = this.recording();
104-
return recording ? this.getLargestSessionShareUrl(recording) : null;
105-
});
106-
public hasRecording: Signal<boolean> = computed(() => this.recordingShareUrl() !== null);
107103
public summary: WritableSignal<PastMeetingSummary | null> = signal(null);
108-
public summaryContent: Signal<string | null> = computed(() => {
109-
const summary = this.summary();
110-
return summary?.summary_data ? summary.summary_data.edited_content || summary.summary_data.content : null;
111-
});
112-
public summaryUid: Signal<string | null> = computed(() => this.summary()?.uid || null);
113-
public summaryApproved: Signal<boolean> = computed(() => this.summary()?.approved || false);
114-
public hasSummary: Signal<boolean> = computed(() => this.summaryContent() !== null);
115104
public additionalRegistrantsCount: WritableSignal<number> = signal(0);
116105
public additionalParticipantsCount: WritableSignal<number> = signal(0);
117106
public actionMenuItems: Signal<MenuItem[]> = this.initializeActionMenuItems();
118107
public attachments: Signal<MeetingAttachment[]> = signal([]);
119108

120109
// Computed values for template
110+
public readonly meetingRegistrantCount: Signal<number> = this.initMeetingRegistrantCount();
111+
public readonly registrantResponseBreakdown: Signal<string> = this.initRegistrantResponseBreakdown();
112+
public readonly summaryContent: Signal<string | null> = this.initSummaryContent();
113+
public readonly summaryUid: Signal<string | null> = this.initSummaryUid();
114+
public readonly summaryApproved: Signal<boolean> = this.initSummaryApproved();
115+
public readonly hasSummary: Signal<boolean> = this.initHasSummary();
121116
public readonly attendancePercentage: Signal<number> = this.initAttendancePercentage();
117+
public readonly recordingShareUrl: Signal<string | null> = this.initRecordingShareUrl();
118+
public readonly hasRecording: Signal<boolean> = this.initHasRecording();
122119
public readonly attendanceBarColor: Signal<string> = this.initAttendanceBarColor();
123120
public readonly totalResourcesCount: Signal<number> = this.initTotalResourcesCount();
124121
public readonly enabledFeaturesCount: Signal<number> = this.initEnabledFeaturesCount();
@@ -132,6 +129,7 @@ export class MeetingCardComponent implements OnInit {
132129
public readonly meetingStartTime: Signal<string | null> = this.initMeetingStartTime();
133130
public readonly canJoinMeeting: Signal<boolean> = this.initCanJoinMeeting();
134131
public readonly joinUrl: Signal<string | null>;
132+
public readonly authenticated: Signal<boolean> = this.userService.authenticated;
135133

136134
public readonly meetingDeleted = output<void>();
137135
public readonly project = this.projectService.project;
@@ -874,4 +872,34 @@ export class MeetingCardComponent implements OnInit {
874872
return canJoinMeeting(this.meeting(), this.occurrence());
875873
});
876874
}
875+
876+
private initRecordingShareUrl(): Signal<string | null> {
877+
return computed(() => {
878+
const recording = this.recording();
879+
return recording ? this.getLargestSessionShareUrl(recording) : null;
880+
});
881+
}
882+
883+
private initHasRecording(): Signal<boolean> {
884+
return computed(() => this.recordingShareUrl() !== null);
885+
}
886+
887+
private initSummaryContent(): Signal<string | null> {
888+
return computed(() => {
889+
const summary = this.summary();
890+
return summary?.summary_data ? summary.summary_data.edited_content || summary.summary_data.content : null;
891+
});
892+
}
893+
894+
private initSummaryUid(): Signal<string | null> {
895+
return computed(() => this.summary()?.uid || null);
896+
}
897+
898+
private initSummaryApproved(): Signal<boolean> {
899+
return computed(() => this.summary()?.approved || false);
900+
}
901+
902+
private initHasSummary(): Signal<boolean> {
903+
return computed(() => this.summaryContent() !== null);
904+
}
877905
}

apps/lfx-one/src/app/shared/components/radio-button/radio-button.component.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
@if (form() && control()) {
55
<ng-container [formGroup]="form()!">
66
<div class="flex items-center gap-2">
7-
<p-radioButton [formControlName]="control()!" [name]="name()" [value]="value()" [inputId]="inputId() || control()!" (onChange)="onRadioChange($event)">
8-
</p-radioButton>
7+
<p-radioButton [formControlName]="control()!" [value]="value()" [inputId]="inputId() || control()!" (onChange)="onRadioChange($event)"> </p-radioButton>
98
@if (label()) {
109
<label [for]="inputId() || control()!" class="text-sm font-medium text-gray-700 cursor-pointer">
1110
{{ label() }}

0 commit comments

Comments
 (0)