diff --git a/src/Client/src/app/api/models.ts b/src/Client/src/app/api/models.ts index 9c71975..1142478 100644 --- a/src/Client/src/app/api/models.ts +++ b/src/Client/src/app/api/models.ts @@ -7,6 +7,7 @@ export { BookRecommendationForMeetingDto } from './models/book-recommendation-fo export { BookVoteDto } from './models/book-vote-dto'; export { CreateBookDto } from './models/create-book-dto'; export { CreateBookVoteDto } from './models/create-book-vote-dto'; +export { CreateMeetingDto } from './models/create-meeting-dto'; export { MeetingDto } from './models/meeting-dto'; export { MeetingSimpleDto } from './models/meeting-simple-dto'; export { MeetingUserStateDto } from './models/meeting-user-state-dto'; diff --git a/src/Client/src/app/api/models/create-meeting-dto.ts b/src/Client/src/app/api/models/create-meeting-dto.ts new file mode 100644 index 0000000..f1fd1e6 --- /dev/null +++ b/src/Client/src/app/api/models/create-meeting-dto.ts @@ -0,0 +1,6 @@ +/* tslint:disable */ +/* eslint-disable */ +export interface CreateMeetingDto { + dateTime: string; + previousMeetingId?: string | null; +} diff --git a/src/Client/src/app/meetings/live-meeting/live-meeting.component.html b/src/Client/src/app/meetings/live-meeting/live-meeting.component.html index 4040bf6..5b513df 100644 --- a/src/Client/src/app/meetings/live-meeting/live-meeting.component.html +++ b/src/Client/src/app/meetings/live-meeting/live-meeting.component.html @@ -71,31 +71,48 @@

And the winner is...

[book]="meeting.winningBook" [ripple]="true"> -
-
Recommendations
- -
} +
+
Recommendations
+ +
} }
-
Join!
- + @if (meeting.status !== MeetingState.Closed) { +
Join!
+ + } @else { + @if (nextMeeting$ | async; as nextMeeting) { +
Next Meeting
+ + } @else { +
Let's do it again!
+ + + + } + }

Roll Call!

diff --git a/src/Client/src/app/meetings/live-meeting/live-meeting.component.ts b/src/Client/src/app/meetings/live-meeting/live-meeting.component.ts index 602daea..c5d8086 100644 --- a/src/Client/src/app/meetings/live-meeting/live-meeting.component.ts +++ b/src/Client/src/app/meetings/live-meeting/live-meeting.component.ts @@ -1,8 +1,10 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; +import { NgbDateStruct, NgbTimeStruct } from '@ng-bootstrap/ng-bootstrap'; import { Store } from '@ngrx/store'; +import moment from 'moment'; import { Observable, Subscription } from 'rxjs'; -import { BookDto, MeetingDto } from '../../api/models'; +import { BookDto, CreateMeetingDto, MeetingDto } from '../../api/models'; import { AppState } from '../../app-state'; import { selectMyRecommendations } from '../../books/state/books.selectors'; import { LiveMeetingService } from '../../services/websockets/live-meeting.service'; @@ -13,6 +15,7 @@ import { MeetingState } from '../state/meetings.reducer'; import { selectLiveMeetingConnected, selectMeetingState, + selectNextMeeting, } from '../state/meetings.selectors'; @Component({ @@ -26,6 +29,8 @@ export class LiveMeetingComponent implements OnInit, OnDestroy { liveMeetingConnected$: Observable = this.store.select( selectLiveMeetingConnected ); + nextMeeting$: Observable = + this.store.select(selectNextMeeting); lastBook: BookDto | null = null; MeetingState = MeetingStatus; @@ -36,6 +41,9 @@ export class LiveMeetingComponent implements OnInit, OnDestroy { myRecommendedBookId?: string; routeUrl: URL; + nextMeetingDate: NgbDateStruct; + nextMeetingTime: NgbTimeStruct = { hour: 18, minute: 0, second: 0 }; + constructor( private liveMeetingSvc: LiveMeetingService, private store: Store, @@ -77,6 +85,13 @@ export class LiveMeetingComponent implements OnInit, OnDestroy { this.myRecommendedBookId = myRecommendation?.book.id; }) ); + + const nextDate = moment().add(5, 'weeks'); + this.nextMeetingDate = { + year: nextDate.year(), + month: nextDate.month() + 1, + day: nextDate.date(), + }; } ngOnInit() { @@ -146,4 +161,21 @@ export class LiveMeetingComponent implements OnInit, OnDestroy { console.log('Winner announced', meeting.winningBook?.title); this.store.dispatch(handleMeetingUpdate({ meeting })); } + + scheduleNextMeeting() { + if (!this.meetingId) { + return; + } + + const nextMeetingDateTime = moment( + `${this.nextMeetingDate.year}-${this.nextMeetingDate.month}-${this.nextMeetingDate.day} ${this.nextMeetingTime.hour}:${this.nextMeetingTime.minute}` + ); + + const newMeeting: CreateMeetingDto = { + dateTime: nextMeetingDateTime.toISOString(), + previousMeetingId: this.meetingId, + }; + + this.liveMeetingSvc.createNextMeeting(newMeeting); + } } diff --git a/src/Client/src/app/meetings/meetings.module.ts b/src/Client/src/app/meetings/meetings.module.ts index 82ce531..83764ca 100644 --- a/src/Client/src/app/meetings/meetings.module.ts +++ b/src/Client/src/app/meetings/meetings.module.ts @@ -1,6 +1,12 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; +import { + NgbDatepickerModule, + NgbModule, + NgbTimepickerModule, +} from '@ng-bootstrap/ng-bootstrap'; import { EffectsModule } from '@ngrx/effects'; import { StoreModule } from '@ngrx/store'; import { QRCodeModule } from 'angularx-qrcode'; @@ -24,6 +30,10 @@ import { meetingsReducer } from './state/meetings.reducer'; BooksModule, DndModule, QRCodeModule, + NgbModule, + NgbTimepickerModule, + NgbDatepickerModule, + FormsModule, ], declarations: [ MeetingCountdownComponent, diff --git a/src/Client/src/app/meetings/state/meetings.reducer.ts b/src/Client/src/app/meetings/state/meetings.reducer.ts index 7c04f6f..39732a1 100644 --- a/src/Client/src/app/meetings/state/meetings.reducer.ts +++ b/src/Client/src/app/meetings/state/meetings.reducer.ts @@ -54,11 +54,7 @@ export const meetingsReducer = createReducer( meetingsActions.handleMeetingUpdate, (state, action): MeetingsState => ({ ...state, - allMeetingStates: state.allMeetingStates.map(meetingState => - meetingState.meeting.id === action.meeting.id - ? { meeting: action.meeting, error: null } - : meetingState - ), + allMeetingStates: handleMeetingUpdate(state, action), }) ), on( @@ -80,3 +76,30 @@ export const meetingsReducer = createReducer( }) ) ); + +function handleMeetingUpdate( + state: MeetingsState, + action: { + meeting: MeetingDto; + } +): MeetingState[] { + const existingMeetingState = state.allMeetingStates.find( + ms => ms.meeting.id === action.meeting.id + ); + + if (!existingMeetingState) { + return [ + ...state.allMeetingStates, + { + meeting: action.meeting, + error: null, + }, + ]; + } + + return state.allMeetingStates.map(meetingState => + meetingState.meeting.id === action.meeting.id + ? { meeting: action.meeting, error: null } + : meetingState + ); +} diff --git a/src/Client/src/app/navbar/navbar.component.html b/src/Client/src/app/navbar/navbar.component.html index 523a314..5b63b26 100644 --- a/src/Client/src/app/navbar/navbar.component.html +++ b/src/Client/src/app/navbar/navbar.component.html @@ -62,11 +62,11 @@ @if ((verified$ | async) && (nextMeeting$ | async); as nextMeeting) {
- Next meeting in !!
} diff --git a/src/Client/src/app/services/websockets/live-meeting.service.ts b/src/Client/src/app/services/websockets/live-meeting.service.ts index 78ef05c..21e8062 100644 --- a/src/Client/src/app/services/websockets/live-meeting.service.ts +++ b/src/Client/src/app/services/websockets/live-meeting.service.ts @@ -1,6 +1,10 @@ import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; -import { CreateBookVoteDto, MeetingDto } from '../../api/models'; +import { + CreateBookVoteDto, + CreateMeetingDto, + MeetingDto, +} from '../../api/models'; import { AppState } from '../../app-state'; import * as actions from '../../meetings/state/meetings.actions'; import { SignalRService } from './signal-r.service'; @@ -79,4 +83,8 @@ export class LiveMeetingService extends SignalRService { async resetMeeting(meetingId: string): Promise { await this.connection.invoke('ResetMeeting', meetingId); } + + async createNextMeeting(meeting: CreateMeetingDto): Promise { + await this.connection.invoke('CreateNextMeeting', meeting); + } } diff --git a/src/Server/WebApi/Dtos/CreateMeetingDto.cs b/src/Server/WebApi/Dtos/CreateMeetingDto.cs index d9064f2..29add6c 100644 --- a/src/Server/WebApi/Dtos/CreateMeetingDto.cs +++ b/src/Server/WebApi/Dtos/CreateMeetingDto.cs @@ -5,7 +5,7 @@ namespace Cbc.WebApi.Dtos; public class CreateMeetingDto : IMapTo { - public Guid PreviousMeetingId { get; set; } + public Guid? PreviousMeetingId { get; set; } public DateTime DateTime { get; set; } } diff --git a/src/Server/WebApi/Helpers/AdditionalSchemaDefinitionsDocumentFilter.cs b/src/Server/WebApi/Helpers/AdditionalSchemaDefinitionsDocumentFilter.cs index eaa8462..e55bf9d 100644 --- a/src/Server/WebApi/Helpers/AdditionalSchemaDefinitionsDocumentFilter.cs +++ b/src/Server/WebApi/Helpers/AdditionalSchemaDefinitionsDocumentFilter.cs @@ -9,7 +9,7 @@ public class AdditionalSchemaDefinitionsDocumentFilter : IDocumentFilter public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) { // add types to this list to expose them in the swagger doc when they aren't used in any controllers - var types = new[] { typeof(CreateBookVoteDto) }; + var types = new[] { typeof(CreateBookVoteDto), typeof(CreateMeetingDto) }; foreach (var type in types) { diff --git a/src/Server/WebApi/Hubs/LiveMeetingHub.cs b/src/Server/WebApi/Hubs/LiveMeetingHub.cs index 99f30ac..d56eb81 100644 --- a/src/Server/WebApi/Hubs/LiveMeetingHub.cs +++ b/src/Server/WebApi/Hubs/LiveMeetingHub.cs @@ -92,7 +92,13 @@ await this.Clients [Authorize(Roles = "Admin")] public async Task CreateNextMeeting(CreateMeetingDto meetingDto) { - var previousMeeting = await dbContext.Meetings.FindAsync(meetingDto.PreviousMeetingId); + if (!meetingDto.PreviousMeetingId.HasValue) + { + await this.Error($"Previous meeting id is required."); + return; + } + + var previousMeeting = await LiveMeetingHubExtensions.GetMeeting(dbContext, meetingDto.PreviousMeetingId.Value); if (previousMeeting is null) { @@ -109,15 +115,16 @@ public async Task CreateNextMeeting(CreateMeetingDto meetingDto) dbContext.Meetings.Add(meeting); await dbContext.SaveChangesAsync(); - var meetingResponseDto = mapper.Map(meeting); + var previousMeetingDto = mapper.Map(previousMeeting); + var newMeetingDto = mapper.Map(meeting); await this.Clients .All - .SendAsync(ClientMethods.MeetingUpdate, previousMeeting, this.Context.ConnectionId); + .SendAsync(ClientMethods.MeetingUpdate, previousMeetingDto, this.Context.ConnectionId); await this.Clients .All - .SendAsync(ClientMethods.MeetingUpdate, meetingResponseDto, this.Context.ConnectionId); + .SendAsync(ClientMethods.MeetingUpdate, newMeetingDto, this.Context.ConnectionId); } [Authorize(Roles = "Admin")]