Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: various fixes and improvements #96

Merged
merged 5 commits into from
Jun 23, 2024
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
7 changes: 5 additions & 2 deletions src/client/src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { routes } from './app.routes';
import { environment } from './environments/environment';
import { AuthInterceptor } from './services/auth.interceptor';
import { AuthService } from './services/auth.service';
import { ThemeService } from './services/theme.service';
import { WebPushService } from './services/web-push.service';

export const appConfig: ApplicationConfig = {
Expand Down Expand Up @@ -58,11 +59,13 @@ export const appConfig: ApplicationConfig = {
provide: APP_INITIALIZER,
multi: true,
useFactory: () => {
// Inject services that need to be initialized
inject(WebPushService);
inject(ThemeService);

const authService = inject(AuthService);
const webPushService = inject(WebPushService);
return async () => {
await authService.init();
webPushService.init();
};
},
},
Expand Down
105 changes: 52 additions & 53 deletions src/client/src/app/components/app/menu/menu.component.html
Original file line number Diff line number Diff line change
@@ -1,24 +1,65 @@
@if (isAdmin()) {
<p-menubar class="relative block" [model]="menuItems()" [autoDisplay]="false">
<p-menubar class="relative block" [model]="adminMenuItems()" [autoDisplay]="false">
<ng-template pTemplate="end">
<div class="flex flex-row flex-wrap items-baseline justify-end gap-x-4">
@if (title()) {
<span class="font-semibold">{{ title() }}</span>
<span class="ml-8 text-xs">{{ translations.title() }}</span>
} @else {
<span class="font-semibold">{{ translations.title() }}</span>
<div class="flex flex-row gap-4">
<div class="flex flex-row flex-wrap items-baseline justify-end gap-x-4 text-end">
@if (title()) {
<span class="font-semibold">{{ title() }}</span>
<span class="ml-8 text-xs">{{ translations.title() }}</span>
} @else {
<span class="font-semibold">{{ translations.title() }}</span>
}
</div>
@if (!isServerConnected()) {
<p-button
[icon]="'i-[mdi--wifi-off]'"
[rounded]="true"
[text]="true"
[severity]="'danger'"
class="-my-8 self-center"
styleClass="text-[1.5rem]"
(onClick)="offlinePanel.toggle($event)"
/>
<p-overlayPanel #offlinePanel [dismissable]="true">
<span>{{ translations.nav_offline() }}</span>
</p-overlayPanel>
}
</div>
<ng-container *ngTemplateOutlet="updateButtonTemplate"></ng-container>
</ng-template>
</p-menubar>
} @else {
<div class="relative flex flex-row gap-2">
@for (item of menuItems(); track index; let index = $index) {
<ng-container
*ngTemplateOutlet="menuItemTemplate; context: { $implicit: item, index }"
></ng-container>
<p-button
[icon]="'i-[mdi--home]'"
[pTooltip]="translations.nav_home()"
[rounded]="true"
[text]="true"
styleClass="h-14 w-14 p-4 text-[1.5rem]"
[routerLink]="'/home'"
/>
<div class="grow"></div>
@if (!isServerConnected()) {
<p-button
[icon]="'i-[mdi--wifi-off]'"
[rounded]="true"
[text]="true"
[severity]="'danger'"
styleClass="h-14 w-14 p-4 text-[1.5rem]"
(onClick)="offlinePanel.toggle($event)"
/>
<p-overlayPanel #offlinePanel [dismissable]="true">
<span>{{ translations.nav_offline() }}</span>
</p-overlayPanel>
}
<p-button
[icon]="'i-[mdi--cog]'"
[pTooltip]="translations.nav_settings()"
[rounded]="true"
[text]="true"
styleClass="h-14 w-14 p-4 text-[1.5rem]"
[routerLink]="'/user-settings'"
/>
<ng-container *ngTemplateOutlet="updateButtonTemplate"></ng-container>
</div>
}
Expand All @@ -39,45 +80,3 @@
</div>
}
</ng-template>

<ng-template #menuItemTemplate let-item let-index="index">
@if (item.visible !== false) {
@if (item.separator) {
@if (item.state?.['grow']) {
<div class="grow"></div>
} @else {
<div class="mx-4 my-auto h-8 w-[1px] bg-surface-d"></div>
}
} @else if (item.items) {
@if (item.state?.['expand'] === true) {
@for (subItem of item.items; track index; let index = $index) {
<ng-container
*ngTemplateOutlet="menuItemTemplate; context: { $implicit: subItem, index }"
></ng-container>
}
} @else {
<div>
<p-button
[icon]="item.icon"
[pTooltip]="item.label"
[rounded]="true"
[text]="true"
styleClass="h-14 w-14 p-4 text-[1.5rem]"
(onClick)="menu.toggle($event)"
/>
<p-menu #menu [model]="item.items" [popup]="true" />
</div>
}
} @else {
<p-button
[icon]="item.icon"
[pTooltip]="item.label"
[rounded]="true"
[text]="true"
styleClass="h-14 w-14 p-4 text-[1.5rem]"
[routerLink]="item.routerLink"
(onClick)="item.command?.({ originalEvent: $event, item, index })"
/>
}
}
</ng-template>
21 changes: 11 additions & 10 deletions src/client/src/app/components/app/menu/menu.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { MenuItem } from 'primeng/api';
import { ButtonModule } from 'primeng/button';
import { MenuModule } from 'primeng/menu';
import { MenubarModule } from 'primeng/menubar';
import { OverlayPanelModule } from 'primeng/overlaypanel';
import { TooltipModule } from 'primeng/tooltip';
import { filter, fromEvent, map, merge } from 'rxjs';

Expand All @@ -20,7 +21,14 @@ import { chainSignals } from '../../../utils/signal.utils';
@Component({
selector: 'app-menu',
standalone: true,
imports: [ButtonModule, CommonModule, MenubarModule, MenuModule, TooltipModule],
imports: [
ButtonModule,
CommonModule,
MenubarModule,
MenuModule,
TooltipModule,
OverlayPanelModule,
],
templateUrl: './menu.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [
Expand Down Expand Up @@ -56,13 +64,13 @@ export class MenuComponent {
),
{ initialValue: false }
);
protected readonly isServerConnected = this._realtimeEventsService.isConnected;

protected readonly menuItems = computed<MenuItem[]>(() => [
protected readonly adminMenuItems = computed<MenuItem[]>(() => [
{
label: this.translations.nav_home(),
icon: 'i-[mdi--home]',
routerLink: '/home',
visible: this.isLoggedIn(),
},
{
label: this.translations.nav_manage(),
Expand All @@ -84,18 +92,11 @@ export class MenuComponent {
routerLink: '/manage/events',
},
],
visible: this.isAdmin(),
},
{
separator: true,
visible: !this.isAdmin(),
state: { grow: true },
},
{
label: this.translations.nav_settings(),
icon: 'i-[mdi--cog]',
routerLink: '/user-settings',
visible: this.isLoggedIn(),
},
]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,25 @@
<span>{{ event.registrationDeadline | date: 'medium' : undefined : locale() }}</span>
</div>
@if (canStart()) {
<p-button
class="self-center"
icon="i-[mdi--play]"
[label]="translations.events_start()"
(onClick)="startEvent()"
[loading]="isStartBusy()"
[disabled]="!allowToStart()"
/>
<div class="flex flex-col gap-2">
<p-button
class="self-center"
icon="i-[mdi--play]"
[label]="translations.events_start()"
(onClick)="startEvent()"
[loading]="isStartBusy()"
[disabled]="!allowToStart()"
/>
@if (!hasInstances()) {
<small class="text-warning text-center">{{
translations.events_warning_notStartableNoInstances()
}}</small>
} @else if (!allTimeslotsHaveMaps()) {
<small class="text-warning text-center">{{
translations.events_warning_notStartableMissingMap()
}}</small>
}
</div>
}
@if (canCommit()) {
<p-button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,18 @@ export class EventDetailsComponent {
protected readonly hasInstances = computed(() =>
this.timeslots().some(x => x.instances.length > 0)
);
protected readonly allTimeslotsHaveMaps = computed(
() =>
!this.event()
?.timeslots.filter(x => x.instances.length > 0)
.some(x => x.mapId === null || x.mapId === undefined)
);

protected readonly canBuildInstances = computed(() =>
ifTruthy(this.event(), event => event.registrationDeadline.getTime() < this.now(), false)
);
protected readonly canStart = computed(
() =>
this.canBuildInstances() && this.event() && !this.event()?.startedAt && this.hasInstances()
() => this.canBuildInstances() && this.event() && !this.event()?.startedAt
);

protected readonly canCommit = computed(
Expand All @@ -103,10 +108,7 @@ export class EventDetailsComponent {
);

protected readonly allowToStart = computed(
() =>
!this.event()
?.timeslots.filter(x => x.instances.length > 0)
.some(x => x.mapId === null || x.mapId === undefined)
() => this.hasInstances() && this.allTimeslotsHaveMaps()
);

constructor() {
Expand Down
7 changes: 6 additions & 1 deletion src/client/src/app/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"eventTimeslot": "Zeitslot",
"maps": "Bahnen",
"users": "Benutzer",
"settings": "Einstellungen"
"settings": "Einstellungen",
"offline": "Die Verbindung zum Server wurde unterbrochen. Die Verbindung wird automatisch wiederhergestellt, sobald sie wieder verfügbar ist."
},
"settings": {
"general": {
Expand Down Expand Up @@ -130,6 +131,10 @@
"title": "Veranstaltung freigeben",
"text": "Möchtest du die Veranstlaung {{date}} wirklich freigeben? Die Zeitfenster können nicht mehr angepasst werden und die Spieler werden über ein neues Event benachrichtigt"
},
"warning": {
"notStartableNoInstances": "Die Veranstaltung kann nicht gestartet werden, da die Gruppen noch nicht gebildet wurden.",
"notStartableMissingMap": "Die Veranstaltung kann nicht gestartet werden, da nicht für alle gespielten Zeitslots eine Bahn festgelegt wurde."
},
"error": {
"load": "Fehler beim Laden der Veranstaltungen.",
"loadOne": "Fehler beim Laden der Veranstaltung.",
Expand Down
7 changes: 6 additions & 1 deletion src/client/src/app/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"eventTimeslot": "Timeslot",
"maps": "Maps",
"users": "Users",
"settings": "Settings"
"settings": "Settings",
"offline": "The connection to the server has been lost. The connection will be restored automatically once it is available again."
},
"settings": {
"general": {
Expand Down Expand Up @@ -130,6 +131,10 @@
"title": "Commit event",
"text": "Do you really want to release the event {{date}}? The time slots can no longer be adjusted and players will be notified of a new event"
},
"warning": {
"notStartableNoInstances": "The event cannot be started because the groups have not been built yet.",
"notStartableMissingMap": "The event cannot be started because not all played timeslots have a map assigned."
},
"error": {
"load": "Failed to load events.",
"loadOne": "Failed to load event.",
Expand Down
26 changes: 17 additions & 9 deletions src/client/src/app/services/auth.interceptor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { from, Observable, switchMap } from 'rxjs';

import { AuthService } from './auth.service';

Expand All @@ -9,14 +9,22 @@ export class AuthInterceptor implements HttpInterceptor {
private readonly _authService = inject(AuthService);

public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = this._authService.token()?.token;
if (token && !req.headers.has('Authorization')) {
req = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`,
},
});
if (req.url.includes('api/auth/token')) {
return next.handle(req);
}
return next.handle(req);

return from(this._authService.ensureTokenNotExpired()).pipe(
switchMap(() => {
const token = this._authService.token()?.token;
if (token && !req.headers.has('Authorization')) {
req = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`,
},
});
}
return next.handle(req);
})
);
}
}
Loading
Loading