Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 32 additions & 14 deletions apps/lfx-pcc/src/app/modules/meeting/meeting.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@
<!-- SPDX-License-Identifier: MIT -->

@if (meeting()) {
<div class="min-h-screen bg-gray-50">
<div class="bg-gray-50">
<!-- Main Content -->
<div class="container mx-auto py-6 px-8">
<div class="max-w-4xl mx-auto">
<!-- Project Logo -->
@if (project()?.logo_url) {
<img src="{{ project()?.logo_url }}" alt="{{ project()?.name }}" class="w-full h-20 mb-6" />
}

<!-- Meeting Information Card -->
<lfx-card class="mb-6" data-testid="meeting-info-card">
<!-- Header with badges -->
Expand Down Expand Up @@ -40,8 +45,8 @@
</div>

<!-- Meeting Title and Date -->
<div class="flex items-start justify-between mb-4" data-testid="meeting-title-section">
<div class="flex-1 pr-4">
<div class="flex sm:flex-row flex-col items-start justify-between mb-4" data-testid="meeting-title-section">
<div class="flex-1 pr-4 flex-grow">
<h2 class="text-xl font-display font-semibold text-gray-900 mb-1 leading-tight" data-testid="meeting-title">
{{ meeting().title }}
</h2>
Expand Down Expand Up @@ -178,7 +183,11 @@ <h4 class="font-medium text-gray-900 font-sans">{{ user.name }}</h4>
<i class="fa-light fa-check-circle"></i>
<span class="font-sans">Ready to join as {{ user.name }}</span>
</div>
<lfx-button size="small" [href]="meeting().join_url" severity="primary" label="Join Meeting" icon="fa-light fa-sign-in"></lfx-button>
@if (meeting().join_url) {
<lfx-button size="small" [href]="meeting().join_url" severity="primary" label="Join Meeting" icon="fa-light fa-sign-in"></lfx-button>
} @else {
<lfx-button size="small" severity="primary" label="Join Meeting" icon="fa-light fa-sign-in" (click)="onJoinMeeting()"></lfx-button>
}
</div>

<div class="text-center">
Expand Down Expand Up @@ -228,13 +237,12 @@ <h4 class="font-medium text-gray-900 font-sans">Enter your information</h4>
<form [formGroup]="joinForm" class="space-y-3">
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label for="fullName" class="flex items-center gap-1 text-sm font-medium text-gray-700 font-sans mb-1">
<label for="name" class="flex items-center gap-1 text-sm font-medium text-gray-700 font-sans mb-1">
<i class="fa-light fa-user text-xs"></i>
Full Name
<span class="text-red-500">*</span>
</label>
<lfx-input-text id="fullName" [form]="joinForm" control="fullName" placeholder="John Doe" styleClass="w-full" size="small">
</lfx-input-text>
<lfx-input-text id="name" [form]="joinForm" control="name" placeholder="John Doe" styleClass="w-full" size="small"> </lfx-input-text>
</div>

<div>
Expand All @@ -258,13 +266,23 @@ <h4 class="font-medium text-gray-900 font-sans">Enter your information</h4>
</div>

<div class="flex items-center justify-end pt-3">
<lfx-button
size="small"
[href]="meeting().join_url"
severity="primary"
label="Join Meeting"
icon="fa-light fa-sign-in"
[disabled]="joinForm.invalid"></lfx-button>
@if (meeting().join_url) {
<lfx-button
size="small"
[href]="joinForm.invalid ? undefined : meeting().join_url"
severity="primary"
label="Join Meeting"
icon="fa-light fa-sign-in"
[disabled]="joinForm.invalid"></lfx-button>
} @else {
<lfx-button
size="small"
severity="primary"
label="Join Meeting"
[disabled]="joinForm.invalid"
icon="fa-light fa-sign-in"
(click)="onJoinMeeting()"></lfx-button>
}
</div>
</form>
</div>
Expand Down
40 changes: 32 additions & 8 deletions apps/lfx-pcc/src/app/modules/meeting/meeting.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { UserService } from '@services/user.service';
import { MessageService } from 'primeng/api';
import { ToastModule } from 'primeng/toast';
import { TooltipModule } from 'primeng/tooltip';
import { combineLatest, map, of, switchMap } from 'rxjs';
import { combineLatest, finalize, map, of, switchMap, tap } from 'rxjs';

@Component({
selector: 'lfx-meeting',
Expand Down Expand Up @@ -51,11 +51,12 @@ export class MeetingComponent {
public authenticated: WritableSignal<boolean>;
public user: Signal<User | null> = this.userService.user;
public joinForm: FormGroup;
public project: Signal<Project | null> = signal<Project | null>(null);
public project: WritableSignal<Project | null> = signal<Project | null>(null);
public meeting: Signal<Meeting & { project: Project }>;
public meetingTypeBadge: Signal<{ badgeClass: string; icon?: string; text: string } | null>;
public importantLinks: Signal<{ url: string; domain: string }[]>;
public returnTo: Signal<string | undefined>;
public password: WritableSignal<string | null> = signal<string | null>(null);

public constructor() {
// Initialize all class variables
Expand All @@ -68,28 +69,51 @@ export class MeetingComponent {
this.returnTo = this.initializeReturnTo();
}

public onJoinMeeting(): void {
this.isJoining.set(true);

this.meetingService
.getPublicMeetingJoinUrl(this.meeting().uid, this.meeting().password, {
email: this.authenticated() ? this.user()?.email : this.joinForm.get('email')?.value,
})
.pipe(finalize(() => this.isJoining.set(false)))
.subscribe({
next: (res) => {
this.meeting().join_url = res.join_url;
window.open(this.meeting().join_url as string, '_blank');
},
error: ({ error }) => {
this.messageService.add({ severity: 'error', summary: 'Error', detail: error.error });
},
});
}

private initializeMeeting() {
return toSignal<Meeting & { project: Project }>(
combineLatest([this.activatedRoute.paramMap, this.activatedRoute.queryParamMap]).pipe(
switchMap(([params]) => {
switchMap(([params, queryParams]) => {
const meetingId = params.get('id');
this.password.set(queryParams.get('password'));
if (meetingId) {
return this.meetingService.getPublicMeeting(meetingId);
return this.meetingService.getPublicMeeting(meetingId, this.password());
}

// TODO: If no meeting ID, redirect to 404
return of({} as { meeting: Meeting; project: Project });
}),
map((res) => ({ ...res.meeting, project: res.project }))
map((res) => ({ ...res.meeting, project: res.project })),
tap((res) => {
this.project.set(res.project);
})
)
) as Signal<Meeting & { project: Project }>;
}

// Private initialization methods
private initializeJoinForm(): FormGroup {
return new FormGroup({
fullName: new FormControl<string>('', [Validators.required]),
email: new FormControl<string>('', [Validators.required, Validators.email]),
name: new FormControl<string>(this.user()?.name || '', [Validators.required]),
email: new FormControl<string>(this.user()?.email || '', [Validators.required, Validators.email]),
organization: new FormControl<string>(''),
});
}
Expand Down Expand Up @@ -154,7 +178,7 @@ export class MeetingComponent {

private initializeReturnTo(): Signal<string | undefined> {
return computed(() => {
return `${environment.urls.home}/meetings/${this.meeting().uid}`;
return `${environment.urls.home}/meetings/${this.meeting().uid}?password=${this.password()}`;
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,28 +80,29 @@
<!-- Meeting Title and Date -->
<div class="flex items-start justify-between mb-4" data-testid="meeting-title-section">
<div class="flex-1 pr-4">
@if (meeting().title) {
@if (occurrence()?.title || meeting().title) {
<h3 class="text-xl font-display font-semibold text-gray-900 mb-1 leading-tight" data-testid="meeting-title">
{{ meeting().title }}
{{ occurrence()?.title || meeting().title }}
</h3>
}
</div>
@if (meeting().start_time) {
@if (occurrence()?.start_time || meeting().start_time) {
<div class="flex items-center gap-1 text-xs text-gray-600 bg-gray-100 px-2 py-1 rounded flex-shrink-0" data-testid="meeting-datetime">
<i class="fa-light fa-calendar-days text-gray-400"></i>
<span
>{{ meeting().start_time | meetingTime: meeting().duration : 'date' }} • {{ meeting().start_time | meetingTime: meeting().duration : 'time' }}</span
>{{ occurrence()?.start_time || meeting().start_time | meetingTime: meeting().duration : 'date' }} •
{{ occurrence()?.start_time || meeting().start_time | meetingTime: meeting().duration : 'time' }}</span
>
</div>
}
</div>

<!-- Description Section -->
@if (meeting().description) {
@if (occurrence()?.description || meeting().description) {
<div class="mb-4" data-testid="meeting-description">
<lfx-expandable-text [maxHeight]="85">
<div class="text-sm text-gray-600 leading-relaxed">
<div [innerHTML]="meeting().description | linkify"></div>
<div [innerHTML]="occurrence()?.description || meeting().description | linkify"></div>
</div>
</lfx-expandable-text>
</div>
Expand Down Expand Up @@ -171,8 +172,8 @@ <h3 class="text-xl font-display font-semibold text-gray-900 mb-1 leading-tight"
<span>Details</span>
</div>
<div class="space-y-1">
@if (meeting().duration) {
<div class="text-sm">{{ meeting().duration }}m duration</div>
@if (occurrence()?.duration || meeting().duration) {
<div class="text-sm">{{ occurrence()?.duration || meeting().duration }}m duration</div>
}
<div class="text-xs text-gray-500">{{ enabledFeaturesCount() }} features enabled</div>
<div class="text-xs text-gray-500">Updated {{ meeting().created_at | date: 'MMM d, y' }}</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ import { AvatarComponent } from '@components/avatar/avatar.component';
import { ButtonComponent } from '@components/button/button.component';
import { ExpandableTextComponent } from '@components/expandable-text/expandable-text.component';
import { MenuComponent } from '@components/menu/menu.component';
import { extractUrlsWithDomains, Meeting, MeetingAttachment, MeetingRegistrant } from '@lfx-pcc/shared';
import { extractUrlsWithDomains, Meeting, MeetingAttachment, MeetingOccurrence, MeetingRegistrant } from '@lfx-pcc/shared';
import { MeetingTimePipe } from '@pipes/meeting-time.pipe';
import { CommitteeService } from '@services/committee.service';
import { MeetingService } from '@services/meeting.service';
import { ProjectService } from '@services/project.service';
import { AnimateOnScrollModule } from 'primeng/animateonscroll';
Expand Down Expand Up @@ -52,19 +51,20 @@ import { RegistrantModalComponent } from '../registrant-modal/registrant-modal.c
export class MeetingCardComponent implements OnInit {
private readonly projectService = inject(ProjectService);
private readonly meetingService = inject(MeetingService);
private readonly committeeService = inject(CommitteeService);
private readonly dialogService = inject(DialogService);
private readonly messageService = inject(MessageService);
private readonly injector = inject(Injector);

public readonly meetingInput = input.required<Meeting>();
public readonly occurrenceInput = input<MeetingOccurrence | null>(null);
public readonly pastMeeting = input<boolean>(false);
public readonly loading = input<boolean>(false);
public readonly showBorder = input<boolean>(false);
public readonly meetingRegistrantCount: Signal<number> = this.initMeetingRegistrantCount();
public readonly registrantResponseBreakdown: Signal<string> = this.initRegistrantResponseBreakdown();
public showRegistrants: WritableSignal<boolean> = signal(false);
public meeting: WritableSignal<Meeting> = signal({} as Meeting);
public occurrence: WritableSignal<MeetingOccurrence | null> = signal(null);
public registrantsLoading: WritableSignal<boolean> = signal(true);
private refresh$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public registrants = this.initRegistrantsList();
Expand Down Expand Up @@ -92,6 +92,9 @@ export class MeetingCardComponent implements OnInit {
public constructor() {
effect(() => {
this.meeting.set(this.meetingInput());
if (this.occurrenceInput()) {
this.occurrence.set(this.occurrenceInput()!);
}
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ <h3 class="text-lg font-semibold text-gray-900 mb-3">Meeting Details</h3>
</div>
</div>

<!-- TODO: Reenable when we have support for deleting series -->
<!-- Recurrence Options -->
@if (isRecurring && !isPastMeeting) {
<!-- @if (isRecurring && !isPastMeeting) {
<div class="mb-6">
<h4 class="text-md font-semibold text-gray-900 mb-3">Delete Options</h4>
<div class="space-y-4">
Expand Down Expand Up @@ -71,7 +72,7 @@ <h4 class="text-md font-semibold text-gray-900 mb-3">Delete Options</h4>
</div>
</div>
</div>
}
} -->

<!-- Warning Message -->
<div class="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { CommonModule } from '@angular/common';
import { Component, inject, signal, WritableSignal } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { ButtonComponent } from '@components/button/button.component';
import { RadioButtonComponent } from '@components/radio-button/radio-button.component';
import { Meeting } from '@lfx-pcc/shared/interfaces';
import { MeetingTimePipe } from '@pipes/meeting-time.pipe';
import { MeetingService } from '@services/meeting.service';
Expand All @@ -21,7 +20,7 @@ export interface MeetingDeleteResult {
@Component({
selector: 'lfx-meeting-delete-confirmation',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, ButtonComponent, RadioButtonComponent, MeetingTimePipe],
imports: [CommonModule, ReactiveFormsModule, ButtonComponent, MeetingTimePipe],
templateUrl: './meeting-delete-confirmation.component.html',
styleUrl: './meeting-delete-confirmation.component.scss',
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
@if (meeting) {
<div class="p-0">
<!-- Show meeting card for view mode -->
<lfx-meeting-card [meetingInput]="meeting" [pastMeeting]="false" (meetingDeleted)="onDelete()"></lfx-meeting-card>
<lfx-meeting-card [meetingInput]="meeting" [occurrenceInput]="occurrence" [pastMeeting]="false" (meetingDeleted)="onDelete()"></lfx-meeting-card>
</div>
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export class MeetingModalComponent {
private readonly dialogRef = inject(DynamicDialogRef);

public readonly meeting = this.config.data?.meeting;
public readonly occurrence = this.config.data?.occurrence;

public onDelete(): void {
this.dialogRef.close(true);
Expand Down
Loading