diff --git a/backend/services/event.py b/backend/services/event.py
index f0bfa4200..8272e8137 100644
--- a/backend/services/event.py
+++ b/backend/services/event.py
@@ -77,9 +77,8 @@ def get_paginated_events(
range_start = pagination_params.range_start
range_end = pagination_params.range_end
criteria = and_(
- EventEntity.time
- >= datetime.strptime(range_start, "%d/%m/%Y, %H:%M:%S"),
- EventEntity.time <= datetime.strptime(range_end, "%d/%m/%Y, %H:%M:%S"),
+ EventEntity.time >= datetime.fromisoformat(range_start),
+ EventEntity.time <= datetime.fromisoformat(range_end),
)
statement = statement.where(criteria)
length_statement = length_statement.where(criteria)
@@ -106,11 +105,13 @@ def get_paginated_events(
limit = pagination_params.page_size
if pagination_params.order_by != "":
- statement = statement.order_by(
- getattr(EventEntity, pagination_params.order_by)
- ) if pagination_params.ascending else statement.order_by(
+ statement = (
+ statement.order_by(getattr(EventEntity, pagination_params.order_by))
+ if pagination_params.ascending
+ else statement.order_by(
getattr(EventEntity, pagination_params.order_by).desc()
)
+ )
statement = statement.offset(offset).limit(limit)
diff --git a/frontend/src/app/admin/users/list/admin-users-list.component.ts b/frontend/src/app/admin/users/list/admin-users-list.component.ts
index 03174cc82..e5eab52ae 100644
--- a/frontend/src/app/admin/users/list/admin-users-list.component.ts
+++ b/frontend/src/app/admin/users/list/admin-users-list.component.ts
@@ -36,7 +36,9 @@ export class AdminUsersListComponent {
canActivate: [permissionGuard('user.list', 'user/')],
resolve: {
page: () =>
- inject(UserAdminService).list(AdminUsersListComponent.PaginationParams)
+ inject(UserAdminService).list(
+ AdminUsersListComponent.PaginationParams as PaginationParams
+ )
}
};
diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts
index a1cc95580..01d2bf24d 100644
--- a/frontend/src/app/app.module.ts
+++ b/frontend/src/app/app.module.ts
@@ -40,7 +40,6 @@ import { ErrorDialogComponent } from './navigation/error-dialog/error-dialog.com
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
import { GateComponent } from './gate/gate.component';
-import { ProfileEditorComponent } from './profile/profile-editor/profile-editor.component';
import { SharedModule } from './shared/shared.module';
@NgModule({
diff --git a/frontend/src/app/event/event-details/event-details.component.html b/frontend/src/app/event/event-details/event-details.component.html
index 5d7cf7c4f..56094aad2 100644
--- a/frontend/src/app/event/event-details/event-details.component.html
+++ b/frontend/src/app/event/event-details/event-details.component.html
@@ -1,9 +1,9 @@
-
+
-
+ @if ((this.canViewEvent() | async) || this.event?.is_organizer ?? false) {
+
+ }
diff --git a/frontend/src/app/event/event-details/event-details.component.ts b/frontend/src/app/event/event-details/event-details.component.ts
index 42c1593c4..899b0e442 100644
--- a/frontend/src/app/event/event-details/event-details.component.ts
+++ b/frontend/src/app/event/event-details/event-details.component.ts
@@ -3,14 +3,13 @@
* any given event.
*
* @author Ajay Gandecha, Jade Keegan, Brianna Ta, Audrey Toney
- * @copyright 2023
+ * @copyright 2024
* @license MIT
*/
-import { Component, inject } from '@angular/core';
-import { profileResolver } from 'src/app/profile/profile.resolver';
-import { eventDetailResolver } from '../event.resolver';
-import { Profile } from 'src/app/profile/profile.service';
+import { Component, OnInit } from '@angular/core';
+import { eventResolver } from '../event.resolver';
+import { Profile, ProfileService } from 'src/app/profile/profile.service';
import {
ActivatedRoute,
ActivatedRouteSnapshot,
@@ -31,46 +30,51 @@ let titleResolver: ResolveFn = (route: ActivatedRouteSnapshot) => {
templateUrl: './event-details.component.html',
styleUrls: ['./event-details.component.css']
})
-export class EventDetailsComponent {
+export class EventDetailsComponent implements OnInit {
/** Route information to be used in Event Routing Module */
public static Route = {
path: ':id',
title: 'Event Details',
component: EventDetailsComponent,
resolve: {
- profile: profileResolver,
- event: eventDetailResolver
+ event: eventResolver
},
children: [
{ path: '', title: titleResolver, component: EventDetailsComponent }
]
};
- /** Store Event */
- public event!: Event;
-
/** Store the currently-logged-in user's profile. */
public profile: Profile;
- public adminPermission$: Observable;
+ /** The event to show */
+ public event: Event | undefined;
+
+ /**
+ * Determines whether or not a user can view the event.
+ * @returns {Observable}
+ */
+ canViewEvent(): Observable {
+ return this.permissionService.check(
+ 'organization.events.view',
+ `organization/${this.event?.organization!?.id ?? '*'}`
+ );
+ }
+
+ /** Constructs the Event Detail component. */
constructor(
private route: ActivatedRoute,
- private permission: PermissionService,
+ private permissionService: PermissionService,
+ private profileService: ProfileService,
private gearService: NagivationAdminGearService
) {
- /** Initialize data from resolvers. */
+ this.profile = this.profileService.profile()!;
+
const data = this.route.snapshot.data as {
- profile: Profile;
event: Event;
};
- this.profile = data.profile;
- this.event = data.event;
- // Admin Permission if has the actual permission or is event organizer
- this.adminPermission$ = this.permission.check(
- 'organization.events.view',
- `organization/${this.event.organization!.id}`
- );
+ this.event = data.event;
}
ngOnInit() {
@@ -78,7 +82,7 @@ export class EventDetailsComponent {
'events.*',
'*',
'',
- `events/organizations/${this.event.organization?.slug}/events/${this.event.id}/edit`
+ `events/${this.event?.organization_id}/${this.event?.id}/edit`
);
}
}
diff --git a/frontend/src/app/event/event-editor/event-editor.component.html b/frontend/src/app/event/event-editor/event-editor.component.html
index 583cb128f..798198530 100644
--- a/frontend/src/app/event/event-editor/event-editor.component.html
+++ b/frontend/src/app/event/event-editor/event-editor.component.html
@@ -1,13 +1,11 @@
-
-
-
-
- You do not have permission to view this page
-
diff --git a/frontend/src/app/event/event-editor/event-editor.component.ts b/frontend/src/app/event/event-editor/event-editor.component.ts
index 3bdab28c9..b1d7128c9 100644
--- a/frontend/src/app/event/event-editor/event-editor.component.ts
+++ b/frontend/src/app/event/event-editor/event-editor.component.ts
@@ -3,7 +3,7 @@
* about events which are publically displayed on the Events page.
*
* @author Ajay Gandecha, Jade Keegan, Brianna Ta, Audrey Toney
- * @copyright 2023
+ * @copyright 2024
* @license MIT
*/
@@ -12,16 +12,16 @@ import { ActivatedRoute, Route, Router } from '@angular/router';
import { FormBuilder, FormControl, Validators } from '@angular/forms';
import { MatSnackBar } from '@angular/material/snack-bar';
import { EventService } from '../event.service';
-import { profileResolver } from '../../profile/profile.resolver';
-import { Profile, PublicProfile } from '../../profile/profile.service';
-import { Observable, map } from 'rxjs';
-import { eventDetailResolver } from '../event.resolver';
-import { PermissionService } from 'src/app/permission.service';
-import { organizationResolver } from 'src/app/organization/organization.resolver';
-import { Organization } from 'src/app/organization/organization.model';
-import { Event, RegistrationType } from '../event.model';
+import {
+ Profile,
+ ProfileService,
+ PublicProfile
+} from '../../profile/profile.service';
+import { eventResolver } from '../event.resolver';
+import { Event } from '../event.model';
import { DatePipe } from '@angular/common';
import { OrganizationService } from 'src/app/organization/organization.service';
+import { eventEditorGuard } from './event-editor.guard';
@Component({
selector: 'app-event-editor',
@@ -29,52 +29,43 @@ import { OrganizationService } from 'src/app/organization/organization.service';
styleUrls: ['./event-editor.component.css']
})
export class EventEditorComponent {
+ /** Route information to be used in Event Routing Module */
public static Route: Route = {
- path: 'organizations/:slug/events/:id/edit',
+ path: ':orgid/:id/edit',
component: EventEditorComponent,
title: 'Event Editor',
+ canActivate: [eventEditorGuard],
resolve: {
- profile: profileResolver,
- organization: organizationResolver,
- event: eventDetailResolver
+ event: eventResolver
}
};
- /** Store the event to be edited or created */
- public event: Event;
- public organization_slug: string;
- public organization: Organization;
-
- public profile: Profile | null = null;
+ /** Store the currently-logged-in user's profile. */
+ public profile: Profile;
- /** Stores whether the user has admin permission over the current organization. */
- public enabled$: Observable;
+ /** Stores the event. */
+ public event: Event;
/** Store organizers */
- public organizers: PublicProfile[] = [];
-
- /** Add validators to the form */
- name = new FormControl('', [Validators.required]);
- time = new FormControl('', [Validators.required]);
- location = new FormControl('', [Validators.required]);
- description = new FormControl('', [
- Validators.required,
- Validators.maxLength(2000)
- ]);
- public = new FormControl('', [Validators.required]);
- registration_limit = new FormControl(0, [
- Validators.required,
- Validators.min(0)
- ]);
-
- /** Create a form group */
+ public organizers: PublicProfile[];
+
+ /** Event Editor Form */
public eventForm = this.formBuilder.group({
- name: this.name,
- time: this.datePipe.transform(new Date(), 'yyyy-MM-ddTHH:mm'),
- location: this.location,
- description: this.description,
- public: this.public.value! == 'true',
- registration_limit: this.registration_limit,
+ name: new FormControl('', [Validators.required]),
+ time: new FormControl(
+ this.datePipe.transform(new Date(), 'yyyy-MM-ddTHH:mm'),
+ [Validators.required]
+ ),
+ location: new FormControl('', [Validators.required]),
+ description: new FormControl('', [
+ Validators.required,
+ Validators.maxLength(2000)
+ ]),
+ public: new FormControl(false, [Validators.required]),
+ registration_limit: new FormControl(0, [
+ Validators.required,
+ Validators.min(0)
+ ]),
userLookup: ''
});
@@ -85,89 +76,54 @@ export class EventEditorComponent {
protected organizationService: OrganizationService,
protected snackBar: MatSnackBar,
private eventService: EventService,
- private permission: PermissionService,
+ private profileService: ProfileService,
private datePipe: DatePipe
) {
- // Get currently-logged-in user
- const data = route.snapshot.data as {
- profile: Profile;
- organization: Organization;
+ this.profile = this.profileService.profile()!;
+
+ const data = this.route.snapshot.data as {
event: Event;
};
- this.profile = data.profile;
-
- // Initialize event
- this.organization = data.organization;
this.event = data.event;
- this.event.organization_id = this.organization.id;
-
- // Get ids from the url
- let organization_slug = this.route.snapshot.params['slug'];
- this.organization_slug = organization_slug;
// Set values for form group
- this.eventForm.setValue({
- name: this.event.name,
- time: this.datePipe.transform(this.event.time, 'yyyy-MM-ddTHH:mm'),
- location: this.event.location,
- description: this.event.description,
- public: this.event.public,
- registration_limit: this.event.registration_limit,
- userLookup: ''
- });
+ this.eventForm.patchValue(
+ Object.assign({}, this.event, {
+ time: this.datePipe.transform(this.event.time, 'yyyy-MM-ddTHH:mm'),
+ userLookup: ''
+ })
+ );
// Add validator for registration_limit
- this.registration_limit.addValidators(
+ this.eventForm.controls['registration_limit'].addValidators(
Validators.min(this.event.registration_count)
);
- this.enabled$ = this.permission
- .check(
- 'organization.events.update',
- `organization/${this.organization!.id}`
- )
- .pipe(map((permission) => permission || this.event.is_organizer));
-
// Set the organizers
// If no organizers already, set current user as organizer
- if (this.event.id == null) {
- let organizer: PublicProfile = {
- id: this.profile.id!,
- first_name: this.profile.first_name!,
- last_name: this.profile.last_name!,
- pronouns: this.profile.pronouns!,
- email: this.profile.email!,
- github_avatar: this.profile.github_avatar
- };
- this.organizers.push(organizer);
- } else {
- // Set organizers to current organizers
- this.organizers = this.event.organizers;
- }
+ this.organizers = this.isNew()
+ ? [this.profile as PublicProfile]
+ : this.event.organizers;
}
- /** Event handler to handle submitting the Create Event Form.
+ /** Event handler to handle submitting the event form.
* @returns {void}
*/
onSubmit() {
if (this.eventForm.valid) {
Object.assign(this.event, this.eventForm.value);
-
- // Set fields not explicitly in form
this.event.organizers = this.organizers;
- if (this.event.id == null) {
- this.eventService.createEvent(this.event).subscribe({
- next: (event) => this.onSuccess(event),
- error: (err) => this.onError(err)
- });
- } else {
- this.eventService.updateEvent(this.event).subscribe({
- next: (event) => this.onSuccess(event),
- error: (err) => this.onError(err)
- });
- }
- this.router.navigate(['/organizations/', this.organization_slug]);
+ let submittedEvent = this.isNew()
+ ? this.eventService.createEvent(this.event)
+ : this.eventService.updateEvent(this.event);
+
+ submittedEvent.subscribe({
+ next: (event) => this.onSuccess(event),
+ error: (err) => this.onError(err)
+ });
+
+ this.router.navigate(['/organizations/', this.event.organization?.slug]);
}
}
@@ -183,17 +139,29 @@ export class EventEditorComponent {
*/
private onSuccess(event: Event): void {
this.router.navigate(['/events/', event.id]);
- if (this.event.id == null) {
- this.snackBar.open('Event Created', '', { duration: 2000 });
- } else {
- this.snackBar.open('Event Edited', '', { duration: 2000 });
- }
+ this.snackBar.open(`Event ${this.action()}`, '', { duration: 2000 });
}
/** Opens a confirmation snackbar when there is an error creating an event.
* @returns {void}
*/
private onError(err: any): void {
- this.snackBar.open('Error: Event Not Created', '', { duration: 2000 });
+ this.snackBar.open(`Error: Event Not ${this.action()}`, '', {
+ duration: 2000
+ });
+ }
+
+ /** Shorthand for whether an event is new or not.
+ * @returns {boolean}
+ */
+ isNew(): boolean {
+ return this.event.id == null;
+ }
+
+ /** Shorthand for determining the action being performed on the event.
+ * @returns {string}
+ */
+ action(): string {
+ return this.isNew() ? 'Created' : 'Updated';
}
}
diff --git a/frontend/src/app/event/event-editor/event-editor.guard.ts b/frontend/src/app/event/event-editor/event-editor.guard.ts
new file mode 100644
index 000000000..08ed56a6c
--- /dev/null
+++ b/frontend/src/app/event/event-editor/event-editor.guard.ts
@@ -0,0 +1,51 @@
+/**
+ * The Event Editor Guard ensures that the page can open if the user has
+ * the correct permissions.
+ *
+ * @author Ajay Gandecha
+ * @copyright 2024
+ * @license MIT
+ */
+
+import { inject } from '@angular/core';
+import { CanActivateFn } from '@angular/router';
+import { PermissionService } from 'src/app/permission.service';
+import { Event } from '../event.model';
+import { combineLatest, map } from 'rxjs';
+import { EventService } from '../event.service';
+
+// TODO: Refactor with a new event permission API so that we do not
+// duplicate calls to the event API here.
+
+/** Determines whether the user can access the event editor.
+ * @param route Active route when the user enters the component.
+ * @returns {CanActivateFn}
+ */
+export const eventEditorGuard: CanActivateFn = (route, _) => {
+ /** Determine if page is viewable by user based on permissions */
+
+ // Load IDs from the route
+ let organizationId: string = route.params['orgid'];
+ let eventId: string = route.params['id'];
+
+ // Create two observables for each check
+
+ // Checks if the user has permissions to update events for
+ // the organization hosting this event
+ const permissionCheck$ = inject(PermissionService).check(
+ 'organization.events.update',
+ `organization/${organizationId}`
+ );
+
+ // Checks if the user is the organizer for the event
+ const isOrganizerCheck$ = inject(EventService)
+ .getEvent(+eventId)
+ .pipe(map((event) => event?.is_organizer ?? false));
+
+ // Since only one check has to be true for the user to see the page,
+ // we combine the results of these observables into a single
+ // observable that returns true if either were true.
+ return combineLatest([permissionCheck$, isOrganizerCheck$]).pipe(
+ map(([hasPermission, isOrganizer]) => hasPermission || isOrganizer)
+ );
+};
diff --git a/frontend/src/app/event/event-list-admin/event-list-admin.component.css b/frontend/src/app/event/event-list-admin/event-list-admin.component.css
deleted file mode 100644
index 2284d548a..000000000
--- a/frontend/src/app/event/event-list-admin/event-list-admin.component.css
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
-* admin-organization-list.component.css
-*
-* The admin organization list page should provide
-* a simple, easily readable form for users to view
-* all organizations.
-*
-*/
-
-.mat-mdc-row .mat-mdc-cell {
- border-bottom: 1px solid transparent;
- border-top: 1px solid transparent;
- cursor: pointer;
-}
-
-.mat-mdc-row:hover .mat-mdc-cell {
- border-color: white;
-}
-
-.header {
- display: flex;
- align-items: center;
- justify-content: space-between;
-}
-
-.row {
- display: flex;
- align-items: center;
- justify-content: space-between;
-}
-
-.button-container button {
- justify-content: space-between;
- margin: 5px;
-}
\ No newline at end of file
diff --git a/frontend/src/app/event/event-list-admin/event-list-admin.component.html b/frontend/src/app/event/event-list-admin/event-list-admin.component.html
deleted file mode 100644
index cc0229657..000000000
--- a/frontend/src/app/event/event-list-admin/event-list-admin.component.html
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
-
{{ element.name }}
-
-
- Edit
-
-
-
-
-
-
-
-
-
diff --git a/frontend/src/app/event/event-list-admin/event-list-admin.component.ts b/frontend/src/app/event/event-list-admin/event-list-admin.component.ts
deleted file mode 100644
index a61a2a99a..000000000
--- a/frontend/src/app/event/event-list-admin/event-list-admin.component.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import { Component, OnInit } from '@angular/core';
-import { ActivatedRoute, Router } from '@angular/router';
-import { MatSnackBar } from '@angular/material/snack-bar';
-import { Observable, map, of } from 'rxjs';
-import {
- Permission,
- Profile
-} from '/workspace/frontend/src/app/profile/profile.service';
-import { Organization } from 'src/app/organization/organization.model';
-import { Event } from 'src/app/event/event.model';
-import { profileResolver } from 'src/app/profile/profile.resolver';
-import { EventService } from 'src/app/event/event.service';
-import { eventResolver } from '../event.resolver';
-import { OrganizationService } from 'src/app/organization/organization.service';
-
-@Component({
- selector: 'app-event-list-admin',
- templateUrl: './event-list-admin.component.html',
- styleUrls: ['./event-list-admin.component.css']
-})
-export class EventListAdminComponent implements OnInit {
- /** Events List */
- protected displayedEvents$: Observable;
-
- public displayedColumns: string[] = ['name'];
-
- /** Profile of signed in user */
- protected profile: Profile;
-
- /** Route information to be used in Organization Routing Module */
- public static Route = {
- path: 'admin',
- component: EventListAdminComponent,
- title: 'Event Administration',
- resolve: {
- profile: profileResolver,
- events: eventResolver
- }
- };
-
- constructor(
- private route: ActivatedRoute,
- private router: Router,
- private snackBar: MatSnackBar,
- private organizationAdminService: OrganizationService,
- private eventService: EventService
- ) {
- this.displayedEvents$ = eventService.getEvents();
-
- /** Get the profile data of the signed in user */
- const data = this.route.snapshot.data as {
- profile: Profile;
- };
- this.profile = data.profile;
- }
-
- ngOnInit() {
- if (this.profile.permissions[0].resource !== '*') {
- let userOrganizationPermissions: string[] = this.profile.permissions
- .filter((permission) => permission.resource.includes('organization'))
- .map((permission) => permission.resource.substring(13));
-
- this.displayedEvents$ = this.displayedEvents$.pipe(
- map((events) =>
- events.filter(
- (event) =>
- event.organization &&
- userOrganizationPermissions.includes(event.organization.slug)
- )
- )
- );
- }
- }
-
- /** Resposible for generating delete and create buttons in HTML code when admin signed in */
- adminPermissions(): boolean {
- return this.profile.permissions[0].resource === '*';
- }
-
- /** Event handler to open Event Editor for the selected event.
- * @param event: event to be edited
- * @returns void
- */
- editEvent(event: Event): void {
- this.router.navigate([
- 'events',
- 'organizations',
- event.organization?.slug,
- 'events',
- event.id,
- 'edit'
- ]);
- }
-}
diff --git a/frontend/src/app/event/event-page/event-page.component.html b/frontend/src/app/event/event-page/event-page.component.html
index 56a65af2d..d79c6871c 100644
--- a/frontend/src/app/event/event-page/event-page.component.html
+++ b/frontend/src/app/event/event-page/event-page.component.html
@@ -3,46 +3,30 @@
+ (searchBarQueryChange)="onSearchBarQueryChange($event)" />
-
-
+
keyboard_arrow_left
-
- Today —
{{ endDate | date: 'mediumDate' }}
+
+ {{ startDate() | date: 'mediumDate' }}
+ —
{{ endDate() | date: 'mediumDate' }}
-
-
- {{ startDate | date: 'mediumDate' }}
- —
- {{ endDate | date: 'mediumDate' }}
-
-
-
+
keyboard_arrow_right
900"
- (cardClicked)="onEventCardClicked($event)"
+ [eventsPerDay]="eventsByDate()"
+ [selectedEvent]="null"
+ [disableLinks]="false"
[fullWidth]="true" />
-
-
-
-
diff --git a/frontend/src/app/event/event-page/event-page.component.ts b/frontend/src/app/event/event-page/event-page.component.ts
index f52501267..e6a374f8c 100644
--- a/frontend/src/app/event/event-page/event-page.component.ts
+++ b/frontend/src/app/event/event-page/event-page.component.ts
@@ -3,268 +3,159 @@
* events hosted by CS Organizations at UNC.
*
* @author Ajay Gandecha, Jade Keegan, Brianna Ta, Audrey Toney
- * @copyright 2023
+ * @copyright 2024
* @license MIT
*/
import {
Component,
- HostListener,
- OnInit,
- inject,
- OnDestroy
+ Signal,
+ signal,
+ effect,
+ WritableSignal,
+ computed
} from '@angular/core';
-import { profileResolver } from 'src/app/profile/profile.resolver';
-import { ActivatedRoute, ActivationEnd, Params, Router } from '@angular/router';
-import { Profile } from 'src/app/profile/profile.service';
+import { ActivatedRoute, Router } from '@angular/router';
+import { Profile, ProfileService } from 'src/app/profile/profile.service';
import { Event } from '../event.model';
import { DatePipe } from '@angular/common';
-import { EventService } from '../event.service';
-import { NagivationAdminGearService } from 'src/app/navigation/navigation-admin-gear.service';
-import { EventPaginationParams, Paginated } from 'src/app/pagination';
import {
- Subject,
- Subscription,
- debounceTime,
- distinctUntilChanged,
- filter,
- tap
-} from 'rxjs';
+ DEFAULT_TIME_RANGE_PARAMS,
+ Paginated,
+ TimeRangePaginationParams
+} from 'src/app/pagination';
+import { EventService } from '../event.service';
+import { GroupEventsPipe } from '../pipes/group-events.pipe';
@Component({
selector: 'app-event-page',
templateUrl: './event-page.component.html',
styleUrls: ['./event-page.component.css']
})
-export class EventPageComponent implements OnInit, OnDestroy {
- public page: Paginated;
- public startDate = new Date();
- public endDate = new Date(new Date().setMonth(new Date().getMonth() + 1));
- public today: boolean = true;
-
- private static EventPaginationParams = {
- order_by: 'time',
- ascending: 'true',
- filter: '',
- range_start: new Date().toLocaleString('en-GB'),
- range_end: new Date(
- new Date().setMonth(new Date().getMonth() + 1)
- ).toLocaleString('en-GB')
- };
-
+export class EventPageComponent {
/** Route information to be used in App Routing Module */
public static Route = {
path: '',
title: 'Events',
component: EventPageComponent,
- canActivate: [],
- resolve: {
- profile: profileResolver,
- page: () =>
- inject(EventService).list(EventPageComponent.EventPaginationParams)
- }
+ canActivate: []
};
- /** Store the content of the search bar */
- public searchBarQuery = '';
+ /** Stores a reactive event pagination page. */
+ public page: WritableSignal<
+ Paginated | undefined
+ > = signal(undefined);
+ private previousParams: TimeRangePaginationParams = DEFAULT_TIME_RANGE_PARAMS;
+
+ /** Stores a reactive mapping of days to events on the active page. */
+ protected eventsByDate: Signal<[string, Event[]][]> = computed(() => {
+ return this.groupEventsPipe.transform(this.page()?.items ?? []);
+ });
- /** Store a map of days to a list of events for that day */
- public eventsPerDay: [string, Event[]][];
+ /** Stores reactive date signals for the bounds of pagination. */
+ public startDate: WritableSignal = signal(new Date());
+ public endDate: WritableSignal = signal(
+ new Date(new Date().setMonth(new Date().getMonth() + 1))
+ );
+ public filterQuery: WritableSignal = signal('');
- /** Store the selected Event */
- public selectedEvent: Event | null = null;
+ /** Store the content of the search bar */
+ public searchBarQuery = '';
/** Store the currently-logged-in user's profile. */
public profile: Profile;
- /** Stores the width of the window. */
- public innerWidth: any;
-
- /** Search bar query string */
- public query: string = '';
-
- public searchUpdate = new Subject();
-
- private routeSubscription!: Subscription;
-
/** Constructor for the events page. */
constructor(
private route: ActivatedRoute,
private router: Router,
public datePipe: DatePipe,
public eventService: EventService,
- private gearService: NagivationAdminGearService
+ private profileService: ProfileService,
+ protected groupEventsPipe: GroupEventsPipe
) {
- // Initialize data from resolvers
- const data = this.route.snapshot.data as {
- profile: Profile;
- page: Paginated;
- };
- this.profile = data.profile;
- this.page = data.page;
- this.today =
- this.startDate.setHours(0, 0, 0, 0) == new Date().setHours(0, 0, 0, 0);
-
- // Group events by their dates
- this.eventsPerDay = eventService.groupEventsByDate(this.page.items);
-
- // Initialize the initially selected event
- if (data.page.items.length > 0) {
- this.selectedEvent = this.page.items[0];
- }
-
- this.searchUpdate
- .pipe(
- filter((search: string) => search.length > 2 || search.length == 0),
- debounceTime(500),
- distinctUntilChanged()
- )
- .subscribe((query) => {
- this.onSearchBarQueryChange(query);
- });
+ this.profile = this.profileService.profile()!;
}
- /** Runs when the frontend UI loads */
- ngOnInit() {
- if (this.profile !== undefined) {
- let userPermissions = this.profile.permissions;
- /** Ensure that the signed in user has permissions before looking at the resource */
- if (userPermissions.length !== 0) {
- /** Admin user, no need to check further */
- if (userPermissions[0].resource === '*') {
- this.gearService.showAdminGearByPermissionCheck(
- 'organizations.*',
- '*',
- '',
- 'events/admin'
- );
- } else {
- /** Find if the signed in user has any organization permissions */
- let organizationPermissions = userPermissions.filter((element) =>
- element.resource.includes('organization')
- );
- /** If they do, show admin gear */
- if (organizationPermissions.length !== 0) {
- this.gearService.showAdminGearByPermissionCheck(
- 'organizations.*',
- organizationPermissions[0].resource,
- '',
- 'events/admin'
- );
- }
- }
- }
- }
- // Keep track of the initial width of the browser window
- this.innerWidth = window.innerWidth;
-
- // Watch current route's query params
- this.route.queryParams.subscribe((params: Params): void => {
- this.startDate = params['start_date']
- ? new Date(Date.parse(params['start_date']))
- : new Date();
- this.endDate = params['end_date']
- ? new Date(Date.parse(params['end_date']))
- : new Date(new Date().setMonth(new Date().getMonth() + 1));
- });
-
- const today = new Date();
- if (this.startDate.getTime() < today.setHours(0, 0, 0, 0)) {
- this.page.params.ascending = 'false';
- }
-
- let paginationParams = this.page.params;
- paginationParams.range_start = this.startDate.toLocaleString('en-GB');
- paginationParams.range_end = this.endDate.toLocaleString('en-GB');
- this.eventService.list(paginationParams).subscribe((page) => {
- this.eventsPerDay = this.eventService.groupEventsByDate(page.items);
+ /**
+ * Effect that refreshes the event pagination when the time range changes. This effect
+ * is also called when the page initially loads.
+ *
+ * This effect also reloads the query parameters in the URL so that the URL in the
+ * browser reflects the newly changed start and end date ranges.
+ */
+ paginationTimeRangeEffect = effect(() => {
+ // Update the parameters with the new date range
+ let params = this.previousParams;
+ params.range_start = this.startDate().toISOString();
+ params.range_end = this.endDate().toISOString();
+ params.filter = this.filterQuery();
+ // Refresh the data
+ this.eventService.getEvents(params).subscribe((events) => {
+ this.page.set(events);
+ this.previousParams = events.params;
+ this.reloadQueryParams();
});
-
- let prevUrl = '';
- this.routeSubscription = this.router.events
- .pipe(
- filter((e) => e instanceof ActivationEnd),
- distinctUntilChanged(() => this.router.url === prevUrl),
- tap(() => (prevUrl = this.router.url))
- )
- .subscribe((_) => {
- this.page.params.ascending = (
- this.startDate.getTime() > today.setHours(0, 0, 0, 0)
- ).toString();
- let paginationParams = this.page.params;
- paginationParams.range_start = this.startDate.toLocaleString('en-GB');
- paginationParams.range_end = this.endDate.toLocaleString('en-GB');
- this.eventService.list(paginationParams).subscribe((page) => {
- this.eventsPerDay = this.eventService.groupEventsByDate(page.items);
- });
- });
+ });
+
+ /** Reloads the page and its query parameters to adjust to the next month. */
+ nextPage() {
+ this.startDate.set(
+ new Date(this.startDate().setMonth(this.startDate().getMonth() + 1))
+ );
+ this.endDate.set(
+ new Date(this.endDate().setMonth(this.endDate().getMonth() + 1))
+ );
}
- ngOnDestroy() {
- this.routeSubscription.unsubscribe();
+ /** Reloads the page and its query parameters to adjust to the previous month. */
+ previousPage() {
+ this.startDate.set(
+ new Date(this.startDate().setMonth(this.startDate().getMonth() - 1))
+ );
+ this.endDate.set(
+ new Date(this.endDate().setMonth(this.endDate().getMonth() - 1))
+ );
}
- /** Handler that runs when the window resizes */
- @HostListener('window:resize', ['$event'])
- onResize(_: UIEvent) {
- // Update the browser window width
- this.innerWidth = window.innerWidth;
+ /**
+ * Reloads the page to update the query parameters and reload the data.
+ * This is required so that the correct query parameters are reflected in the
+ * browser's URL field.
+ * @param startDate: The new start date
+ * @param endDate: The new end date
+ */
+ reloadQueryParams() {
+ this.router.navigate([], {
+ relativeTo: this.route,
+ queryParams: {
+ start_date: this.startDate().toISOString(),
+ end_date: this.endDate().toISOString()
+ },
+ queryParamsHandling: 'merge'
+ });
}
+ // TODO: Refactor this method to remove manual +/- 100 year range on query filtering.
+
/** Handler that runs when the search bar query changes.
* @param query: Search bar query to filter the items
*/
onSearchBarQueryChange(query: string) {
- this.query = query;
- let paginationParams = this.page.params;
- paginationParams.ascending = 'true';
- if (query == '') {
- paginationParams.range_start = this.startDate.toLocaleString('en-GB');
- paginationParams.range_end = this.endDate.toLocaleString('en-GB');
+ if (query === '') {
+ this.startDate.set(new Date());
+ this.endDate.set(
+ new Date(new Date().setMonth(new Date().getMonth() + 1))
+ );
} else {
- paginationParams.range_start = new Date(
- new Date().setFullYear(new Date().getFullYear() - 100)
- ).toLocaleString('en-GB');
- paginationParams.range_end = new Date(
- new Date().setFullYear(new Date().getFullYear() + 100)
- ).toLocaleString('en-GB');
- paginationParams.filter = this.query;
- }
- this.eventService.list(paginationParams).subscribe((page) => {
- this.eventsPerDay = this.eventService.groupEventsByDate(page.items);
- paginationParams.filter = '';
- });
- }
-
- /** Handler that runs when an event card is clicked.
- * This function selects the event to display on the sidebar.
- * @param event: Event pressed
- */
- onEventCardClicked(event: Event) {
- this.selectedEvent = event;
- }
-
- showEvents(isPrevious: boolean) {
- //let paginationParams = this.page.params;
- this.startDate = isPrevious
- ? new Date(this.startDate.setMonth(this.startDate.getMonth() - 1))
- : new Date(this.startDate.setMonth(this.startDate.getMonth() + 1));
- this.endDate = isPrevious
- ? new Date(this.endDate.setMonth(this.endDate.getMonth() - 1))
- : new Date(this.endDate.setMonth(this.endDate.getMonth() + 1));
- if (isPrevious === true) {
- this.page.params.ascending = 'false';
+ this.startDate.set(
+ new Date(new Date().setMonth(new Date().getFullYear() - 100))
+ );
+ this.endDate.set(
+ new Date(new Date().setMonth(new Date().getFullYear() + 100))
+ );
}
- this.today =
- this.startDate.setHours(0, 0, 0, 0) == new Date().setHours(0, 0, 0, 0);
- this.router.navigate([], {
- relativeTo: this.route,
- queryParams: {
- start_date: this.startDate.toISOString(),
- end_date: this.endDate.toISOString()
- },
- queryParamsHandling: 'merge'
- });
+ this.filterQuery.set(query);
}
}
diff --git a/frontend/src/app/event/event-routing.module.ts b/frontend/src/app/event/event-routing.module.ts
index f8dd50367..8cc9ea5cd 100644
--- a/frontend/src/app/event/event-routing.module.ts
+++ b/frontend/src/app/event/event-routing.module.ts
@@ -12,10 +12,8 @@ import { RouterModule, Routes } from '@angular/router';
import { EventDetailsComponent } from './event-details/event-details.component';
import { EventPageComponent } from './event-page/event-page.component';
import { EventEditorComponent } from './event-editor/event-editor.component';
-import { EventListAdminComponent } from './event-list-admin/event-list-admin.component';
const routes: Routes = [
- EventListAdminComponent.Route,
EventPageComponent.Route,
EventDetailsComponent.Route,
EventEditorComponent.Route
diff --git a/frontend/src/app/event/event.model.ts b/frontend/src/app/event/event.model.ts
index 9ada9851c..0d0d12efe 100644
--- a/frontend/src/app/event/event.model.ts
+++ b/frontend/src/app/event/event.model.ts
@@ -3,7 +3,7 @@
* the Event Service and the API.
*
* @author Ajay Gandecha, Jade Keegan, Brianna Ta, Audrey Toney
- * @copyright 2023
+ * @copyright 2024
* @license MIT
*/
@@ -54,8 +54,10 @@ export interface EventJson {
* objects (such as `Date`s) as strings. We need to convert this to
* TypeScript objects ourselves.
*/
-export const parseEventJson = (eventJson: EventJson): Event => {
- return Object.assign({}, eventJson, { time: new Date(eventJson.time) });
+export const parseEventJson = (responseModel: EventJson): Event => {
+ return Object.assign({}, responseModel, {
+ time: new Date(responseModel.time)
+ });
};
export enum RegistrationType {
diff --git a/frontend/src/app/event/event.module.ts b/frontend/src/app/event/event.module.ts
index 91ec7892f..e667ce47a 100644
--- a/frontend/src/app/event/event.module.ts
+++ b/frontend/src/app/event/event.module.ts
@@ -4,7 +4,7 @@
* application and decouples this feature from other features in the application.
*
* @author Ajay Gandecha, Jade Keegan, Brianna Ta, Audrey Toney
- * @copyright 2023
+ * @copyright 2024
* @license MIT
*/
@@ -38,7 +38,7 @@ import { EventDetailsComponent } from './event-details/event-details.component';
import { EventPageComponent } from './event-page/event-page.component';
import { EventEditorComponent } from './event-editor/event-editor.component';
import { EventUsersList } from './widgets/event-users-list/event-users-list.widget';
-import { EventListAdminComponent } from './event-list-admin/event-list-admin.component';
+import { GroupEventsPipe } from './pipes/group-events.pipe';
@NgModule({
declarations: [
@@ -46,8 +46,8 @@ import { EventListAdminComponent } from './event-list-admin/event-list-admin.com
EventDetailsComponent,
EventPageComponent,
EventEditorComponent,
- EventListAdminComponent,
- EventUsersList
+ EventUsersList,
+ GroupEventsPipe
],
imports: [
CommonModule,
@@ -70,6 +70,7 @@ import { EventListAdminComponent } from './event-list-admin/event-list-admin.com
RouterModule,
SharedModule,
EventRoutingModule
- ]
+ ],
+ providers: [GroupEventsPipe]
})
export class EventModule {}
diff --git a/frontend/src/app/event/event.resolver.ts b/frontend/src/app/event/event.resolver.ts
index 8f5fc211f..9db6d8235 100644
--- a/frontend/src/app/event/event.resolver.ts
+++ b/frontend/src/app/event/event.resolver.ts
@@ -3,7 +3,7 @@
* of components.
*
* @author Ajay Gandecha, Jade Keegan, Brianna Ta, Audrey Toney
- * @copyright 2023
+ * @copyright 2024
* @license MIT
*/
@@ -12,17 +12,8 @@ import { ResolveFn } from '@angular/router';
import { Event } from './event.model';
import { EventService } from './event.service';
-/** This resolver injects the list of events into the events component. */
-export const eventResolver: ResolveFn = (route, state) => {
- return inject(EventService).getEvents();
-};
-
/** This resolver injects an event into the events detail component. */
-export const eventDetailResolver: ResolveFn = (
- route,
- state
-) => {
- console.log(route.paramMap);
+export const eventResolver: ResolveFn = (route, state) => {
if (route.paramMap.get('id') != 'new') {
return inject(EventService).getEvent(+route.paramMap.get('id')!);
} else {
diff --git a/frontend/src/app/event/event.service.ts b/frontend/src/app/event/event.service.ts
index 994d02eaf..4f539c6da 100644
--- a/frontend/src/app/event/event.service.ts
+++ b/frontend/src/app/event/event.service.ts
@@ -3,252 +3,132 @@
* from the components.
*
* @author Ajay Gandecha, Jade Keegan, Brianna Ta, Audrey Toney
- * @copyright 2023
+ * @copyright 2024
* @license MIT
*/
import { Injectable } from '@angular/core';
-import { HttpClient } from '@angular/common/http';
-import { Observable, Subscription, map, tap } from 'rxjs';
+import {
+ DEFAULT_TIME_RANGE_PARAMS,
+ Paginated,
+ PaginationParams,
+ Paginator,
+ TimeRangePaginationParams,
+ TimeRangePaginator
+} from '../pagination';
import {
Event,
EventJson,
EventRegistration,
parseEventJson
} from './event.model';
-import { DatePipe } from '@angular/common';
-import { Profile, ProfileService } from '../profile/profile.service';
-import {
- Paginated,
- PaginationParams,
- TimeRangePaginationParams
-} from '../pagination';
-import { RxEvent } from './rx-event';
+import { Observable, map } from 'rxjs';
+import { HttpClient } from '@angular/common/http';
+import { Profile } from '../models.module';
@Injectable({
providedIn: 'root'
})
export class EventService {
- private profile: Profile | undefined;
- private profileSubscription!: Subscription;
+ /** Encapsulated paginators */
+ private eventsPaginator: TimeRangePaginator =
+ new TimeRangePaginator('/api/events/paginate');
- private events: RxEvent = new RxEvent();
- public events$: Observable = this.events.value$;
+ /** Constructor */
+ constructor(protected http: HttpClient) {}
- constructor(
- protected http: HttpClient,
- protected profileSvc: ProfileService,
- public datePipe: DatePipe
- ) {
- this.profileSubscription = this.profileSvc.profile$.subscribe(
- (profile) => (this.profile = profile)
- );
- }
+ // Methods for event data.
- /** Returns paginated user entries from the backend database table using the backend HTTP get request.
- * @returns {Observable>}
+ /**
+ * Retrieves a page of events based on pagination parameters.
+ * @param params: Pagination parameters.
+ * @returns {Observable>}
*/
- getRegisteredUsersForEvent(event_id: number, params: PaginationParams) {
- let paramStrings = {
- page: params.page.toString(),
- page_size: params.page_size.toString(),
- order_by: params.order_by,
- filter: params.filter
- };
- let query = new URLSearchParams(paramStrings);
- return this.http.get>(
- `/api/events/${event_id}/registrations/users?` + query.toString()
- );
+ getEvents(params: TimeRangePaginationParams = DEFAULT_TIME_RANGE_PARAMS) {
+ return this.eventsPaginator.loadPage(params, parseEventJson);
}
- /** Returns all event entries from the backend database table using the backend HTTP get request.
- * @returns {Observable}
+ /**
+ * Gets an event based on its id.
+ * @param id: ID for the event.
+ * @returns {Observable}
*/
- getEvents(): Observable {
- if (this.profile) {
- return this.http
- .get('/api/events/range')
- .pipe(map((eventJsons) => eventJsons.map(parseEventJson)));
- } else {
- // if a user isn't logged in, return the normal endpoint without registration statuses
- return this.http
- .get('/api/events/range/unauthenticated')
- .pipe(map((eventJsons) => eventJsons.map(parseEventJson)));
- }
+ getEvent(id: number): Observable {
+ return this.http
+ .get('/api/events/' + id)
+ .pipe(map((eventJson) => parseEventJson(eventJson)));
}
- /** Returns the event object from the backend database table using the backend HTTP get request.
- * @param id: ID of the event to retrieve
- * @returns {Observable}
- */
- getEvent(id: number): Observable {
- if (this.profile) {
- return this.http
- .get('/api/events/' + id)
- .pipe(map((eventJson) => parseEventJson(eventJson)));
- } else {
- return this.http
- .get('/api/events/' + id + '/unauthenticated')
- .pipe(map((eventJson) => parseEventJson(eventJson)));
- }
- }
-
- /** Returns the event object from the backend database table using the backend HTTP get request.
- * @param slug: Slug of the organization to retrieve
- * @returns {Observable}
- */
- getEventsByOrganization(slug: string): Observable {
- if (this.profile) {
- return this.http
- .get('/api/events/organization/' + slug)
- .pipe(map((eventJsons) => eventJsons.map(parseEventJson)));
- } else {
- return this.http
- .get(
- '/api/events/organization/' + slug + '/unauthenticated'
- )
- .pipe(map((eventJsons) => eventJsons.map(parseEventJson)));
- }
- }
-
- /** Returns the new event object from the backend database table using the backend HTTP get request.
- * @param event: model of the event to be created
+ /**
+ * Returns the new event from the backend database table using the HTTP post request
+ * and refreshes the current paginated events page.
+ * @param event Event to add
* @returns {Observable}
*/
createEvent(event: Event): Observable {
return this.http.post('/api/events', event);
}
- /** Returns the updated event object from the backend database table using the backend HTTP put request.
- * @param event: Event representing the updated event
+ /**
+ * Returns the updated event from the backend database table using the HTTP put request
+ * and refreshes the current paginated events page.
+ * @param event Event to update
* @returns {Observable}
*/
updateEvent(event: Event): Observable {
return this.http.put('/api/events', event);
}
- /** Delete the given event object using the backend HTTP delete request. W
- * @param event: Event representing the updated event
- * @returns void
+ /**
+ * Returns the deleted event from the backend database table using the HTTP delete request
+ * and refreshes the current paginated events page.
+ * @param event Event to delete
+ * @returns {Observable}
*/
deleteEvent(event: Event): Observable {
return this.http.delete('/api/events/' + event.id);
}
- /** Helper function to group a list of events by date,
- * filtered based on the input query string.
- * @param events: List of the input events
- * @param query: Search bar query to filter the items
- */
- groupEventsByDate(events: Event[], query: string = ''): [string, Event[]][] {
- // Initialize an empty map
- let groups: Map = new Map();
+ // Methods for event registration data.
- // Transform the list of events based on the event filter pipe and query
- events.forEach((event) => {
- // Find the date to group by
- let dateString =
- this.datePipe.transform(event.time, 'EEEE, MMMM d, y') ?? '';
- // Add the event
- let newEventsList = groups.get(dateString) ?? [];
- newEventsList.push(event);
- groups.set(dateString, newEventsList);
- });
+ // TODO: Refactor to remove, load event registrations instead.
- // Return the groups
- return [...groups.entries()];
- }
-
- // Event Registration Methods
- /** Return an event registration if the user is registered for an event using the backend HTTP get request.
- * @param event_id: number representing the Event ID
- * @returns Observable
+ /**
+ * Loads a paginated list of registered users for a given event.
+ * @param event: Event to load registrations for.
+ * @param params: Pagination parameters.
+ * @returns {Observable>}
*/
- getEventRegistrationOfUser(event_id: number): Observable {
- return this.http.get(
- `/api/events/${event_id}/registration`
+ getRegisteredUsersForEvent(
+ event: Event,
+ params: PaginationParams
+ ): Observable> {
+ const paginator: Paginator = new Paginator(
+ `/api/events/${event.id}/registrations/users`
);
+ return paginator.loadPage(params);
}
- /** Return all event registrations an event using the backend HTTP get request.
- * @param event_id: number representing the Event ID
- * @returns Observable
+ /**
+ * Registers the current user to an event.
+ * @param event: Event to register to.
+ * @returns {Observable}
*/
- getEventRegistrations(event_id: number): Observable {
- return this.http.get(
- `/api/events/${event_id}/registrations`
- );
- }
-
- /** Return number of event registrations for an event
- * @param event_id: number representing the Event ID
- * @returns Observable
- */
- getEventRegistrationCount(event_id: number): Observable {
- return this.http.get(`/api/events/${event_id}/registration/count`);
- }
-
- /** Create a new registration for an event using the backend HTTP create request.
- * @param event_id: number representing the Event ID
- * @returns Observable
- */
- registerForEvent(event_id: number): Observable {
- if (this.profile === undefined) {
- throw new Error('Only allowed for logged in users.');
- }
-
+ registerForEvent(event: Event): Observable {
return this.http.post(
- `/api/events/${event_id}/registration`,
+ `/api/events/${event.id}/registration`,
{}
);
}
- /** Delete an existing registration for an event using the backend HTTP delete request.
- * @param event_registration_id: number representing the Event Registration ID
- * @returns void
+ /**
+ * Unregisters the current user from an event.
+ * @param event: Event to unregister from.
+ * @returns {Observable}
*/
- unregisterForEvent(event_id: number) {
- if (this.profile === undefined) {
- throw new Error('Only allowed for logged in users.');
- }
-
+ unregisterForEvent(event: Event): Observable {
return this.http.delete(
- `/api/events/${event_id}/registration`
+ `/api/events/${event.id}/registration`
);
}
-
- list(params: TimeRangePaginationParams) {
- let paramStrings = {
- order_by: params.order_by,
- ascending: params.ascending,
- filter: params.filter,
- range_start: params.range_start,
- range_end: params.range_end
- };
- let query = new URLSearchParams(paramStrings);
- if (this.profile) {
- return this.http
- .get>(
- '/api/events/paginate?' + query.toString()
- )
- .pipe(
- map((paginated) => ({
- ...paginated,
- items: paginated.items.map(parseEventJson)
- }))
- );
- } else {
- // if a user isn't logged in, return the normal endpoint without registration statuses
- return this.http
- .get>(
- '/api/events/paginate/unauthenticated?' + query.toString()
- )
- .pipe(
- map((paginated) => ({
- ...paginated,
- items: paginated.items.map(parseEventJson)
- }))
- );
- }
- }
}
diff --git a/frontend/src/app/event/pipes/group-events.pipe.ts b/frontend/src/app/event/pipes/group-events.pipe.ts
new file mode 100644
index 000000000..4ef3330df
--- /dev/null
+++ b/frontend/src/app/event/pipes/group-events.pipe.ts
@@ -0,0 +1,36 @@
+/**
+ * This is the pipe used to group events in a page by day.
+ * @author Ajay Gandecha
+ * @copyright 2024
+ * @license MIT
+ */
+
+import { DatePipe } from '@angular/common';
+import { Pipe, PipeTransform, inject } from '@angular/core';
+import { Event } from '../event.model';
+
+@Pipe({
+ name: 'groupEvents'
+})
+export class GroupEventsPipe implements PipeTransform {
+ datePipe = inject(DatePipe);
+
+ transform(events: Event[]): [string, Event[]][] {
+ // Initialize an empty map
+ let groups: Map = new Map();
+
+ // Transform the list of events based on the event filter pipe and query
+ events.forEach((event) => {
+ // Find the date to group by
+ let dateString =
+ this.datePipe.transform(event.time, 'EEEE, MMMM d, y') ?? '';
+ // Add the event
+ let newEventsList = groups.get(dateString) ?? [];
+ newEventsList.push(event);
+ groups.set(dateString, newEventsList);
+ });
+
+ // Return the groups
+ return [...groups.entries()];
+ }
+}
diff --git a/frontend/src/app/event/rx-event.ts b/frontend/src/app/event/rx-event.ts
deleted file mode 100644
index 45f07eff4..000000000
--- a/frontend/src/app/event/rx-event.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * The RxEvent object is used to ensure proper updating and
- * retrieval of the list of all events in the database.
- *
- * @author Ben Goulet
- * @copyright 2024
- * @license MIT
- */
-
-import { RxObject } from '../rx-object';
-import { Event } from './event.model';
-
-export class RxEvent extends RxObject {
- pushEvent(event: Event): void {
- this.value.push(event);
- this.notify();
- }
-
- updateEvent(event: Event): void {
- this.value = this.value.map((o) => {
- return o.id !== event.id ? o : event;
- });
- this.notify();
- }
-}
diff --git a/frontend/src/app/event/widgets/event-detail-card/event-detail-card.widget.html b/frontend/src/app/event/widgets/event-detail-card/event-detail-card.widget.html
index 0ad6ee763..37ec7de3a 100644
--- a/frontend/src/app/event/widgets/event-detail-card/event-detail-card.widget.html
+++ b/frontend/src/app/event/widgets/event-detail-card/event-detail-card.widget.html
@@ -11,19 +11,14 @@
-
+ @if (this.adminPermission$ | async) {
+
delete
+ }
share
-
-
@@ -55,50 +50,43 @@
-